@animalabs/agent-framework 0.26.2 → 0.27.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.
- package/README.md +56 -6
- package/dist/src/agent.d.ts +263 -22
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +531 -54
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +1 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +33 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +35 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +759 -129
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3482 -928
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +309 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2117 -395
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/types.d.ts +68 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1152 -434
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +736 -77
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +82 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +132 -7
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +259 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +628 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +39 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +292 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +200 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +621 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +80 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +431 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +21 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +23 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -15
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +51 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +181 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +384 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +26 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +58 -28
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +1 -1
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +246 -27
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1216 -236
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +213 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/result-text.d.ts +67 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +41 -17
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +81 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +103 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +4 -2
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +21 -6
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +176 -24
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +411 -91
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +186 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +15 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +24 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +90 -7
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +57 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/agent.ts +593 -60
- package/src/api/server.ts +1 -2
- package/src/auto-continue.ts +42 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/framework.ts +3601 -967
- package/src/gate/event-gate.ts +2315 -389
- package/src/gate/index.ts +2 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/types.ts +70 -8
- package/src/index.ts +7 -4
- package/src/mcpl/channel-registry.ts +1220 -467
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +851 -77
- package/src/mcpl/server-connection.ts +145 -7
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +650 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +285 -0
- package/src/mcpl/toolset-host.ts +637 -0
- package/src/mcpl/toolsets-module.ts +451 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +22 -1
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +77 -24
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +48 -18
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/direct-write.ts +425 -0
- package/src/modules/workspace/edit-text.ts +82 -35
- package/src/modules/workspace/effects.ts +1 -1
- package/src/modules/workspace/index.ts +1274 -241
- package/src/modules/workspace/read-page.ts +222 -0
- package/src/modules/workspace/result-text.ts +119 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +41 -17
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +141 -0
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +19 -6
- package/src/tool-result-history.ts +471 -90
- package/src/tune-out/coordinator.ts +80 -37
- package/src/types/agent.ts +189 -7
- package/src/types/events.ts +15 -1
- package/src/types/framework.ts +21 -3
- package/src/types/module.ts +86 -7
- package/src/types/trace.ts +58 -2
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* retires).
|
|
21
21
|
*
|
|
22
22
|
* The ReferenceRegistry is the host-private reference record (§6.2): raw
|
|
23
|
-
* URIs and testimony live here and
|
|
23
|
+
* URIs and testimony live here, and model-visible content shows a uri only
|
|
24
|
+
* as a tool result's link line shows one (shownLinkUri). Fetching
|
|
24
25
|
* (§6/§7 origin binding, ceilings, redirects) is deliberately not here yet —
|
|
25
26
|
* the registry is what makes a future host-mediated fetcher and the
|
|
26
27
|
* code-execution materialization hook possible.
|
|
27
28
|
*/
|
|
28
|
-
import { NON_STRING_TEXT_PLACEHOLDER } from './visible-content.js';
|
|
29
29
|
export const REFERENCE_LIMITS = { uri: 4096, mimeType: 255, name: 255, expiresAt: 64 };
|
|
30
30
|
export const DIGEST_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
|
|
31
31
|
const MAX_SAFE = 9007199254740991; // 2^53 - 1
|
|
@@ -67,29 +67,77 @@ export function classifyBlock(block) {
|
|
|
67
67
|
: { kind: 'invalid', shown: NON_STRING_TEXT_PLACEHOLDER };
|
|
68
68
|
case 'image':
|
|
69
69
|
case 'audio': {
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
// Inline form claiming bulk disposition (or smuggling a
|
|
70
|
+
// An empty uri is no link, as the push lane's converter reads it.
|
|
71
|
+
const hasLink = typeof block.uri === 'string' && block.uri !== '';
|
|
72
|
+
if (typeof block.data === 'string') {
|
|
73
|
+
// Inline form claiming bulk disposition (or smuggling a link beside
|
|
74
74
|
// the data) is the vector-2 contradiction: fail closed, withhold.
|
|
75
|
-
|
|
76
|
-
// tool-result lane gives every inline shape a marker of its own,
|
|
77
|
-
// showing that data would put some of it into history as raw JSON.
|
|
78
|
-
// Once that lane does, an empty link beside data reads as no link.
|
|
79
|
-
return { kind: 'inline', contradiction: block.disposition !== undefined || hasUri };
|
|
75
|
+
return { kind: 'inline', contradiction: block.disposition !== undefined || hasLink };
|
|
80
76
|
}
|
|
81
|
-
|
|
82
|
-
// reads it.
|
|
83
|
-
if (hasUri && block.uri !== '')
|
|
77
|
+
if (hasLink)
|
|
84
78
|
return parseReferenceFields(block);
|
|
85
79
|
return { kind: 'invalid', shown: block.type === 'image' ? IMAGE_NO_DATA_TEXT : AUDIO_NO_DATA_TEXT };
|
|
86
80
|
}
|
|
87
|
-
case 'resource':
|
|
88
|
-
|
|
81
|
+
case 'resource': {
|
|
82
|
+
const contents = embeddedResourceContents(block);
|
|
83
|
+
return contents ? { kind: 'embedded', contents } : parseReferenceFields(block);
|
|
84
|
+
}
|
|
85
|
+
case 'resource_link':
|
|
86
|
+
// MCP's link to a resource is a reference too. RFC-005 §8.1 maps it
|
|
87
|
+
// onto the reference record field for field, its `size` as `sizeBytes`,
|
|
88
|
+
// with no disposition: the host's default policy, as for any reference
|
|
89
|
+
// that claims none. Its title, description and annotations aren't
|
|
90
|
+
// testimony.
|
|
91
|
+
// A link's size is MCP's `size`; the script lane's object for a link
|
|
92
|
+
// names it `sizeBytes`, so given again it reads the same, where
|
|
93
|
+
// sanitizeLabel left its uri as it was. Read on every lane, so a
|
|
94
|
+
// peer's link that names a `sizeBytes` is labelled with it too.
|
|
95
|
+
return linkTestimony(parseReferenceFields({ uri: block.uri, name: block.name, mimeType: block.mimeType, sizeBytes: block.size ?? block.sizeBytes }));
|
|
89
96
|
default:
|
|
90
97
|
return { kind: 'invalid', shown: unknownBlockText(block) };
|
|
91
98
|
}
|
|
92
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* MCP's EmbeddedResource: a `resource` block with no uri of its own whose
|
|
102
|
+
* `resource` is a record with a string `uri`. RFC-005 §8.1 reads it as "an
|
|
103
|
+
* ordinary inline block, nothing new", so its contents show as the block
|
|
104
|
+
* they make: with exactly one of a string `text` or a string `blob`
|
|
105
|
+
* (base64), its text as text, and its blob as the inline image or audio its
|
|
106
|
+
* media type makes it, or as a marker for any other type; with both or
|
|
107
|
+
* neither, the marker saying its contents can't be read. Its uri is never shown, since
|
|
108
|
+
* no lane RFC-005 governs showed it before and nothing here can tell a
|
|
109
|
+
* capability in a uri from a location. A disposition on it is inline
|
|
110
|
+
* content claiming bulk disposition (RFC-005 vector 2), so it's withheld.
|
|
111
|
+
* Null for any other block, among them MCPL's reference, whose uri is its
|
|
112
|
+
* own.
|
|
113
|
+
*/
|
|
114
|
+
function embeddedResourceContents(block) {
|
|
115
|
+
if (block.uri !== undefined && block.uri !== null && block.uri !== '')
|
|
116
|
+
return null;
|
|
117
|
+
if (!isRecord(block.resource) || typeof block.resource.uri !== 'string')
|
|
118
|
+
return null;
|
|
119
|
+
const { text, blob, mimeType } = block.resource;
|
|
120
|
+
if (block.disposition !== undefined)
|
|
121
|
+
return { kind: 'withheld' };
|
|
122
|
+
if ((typeof text === 'string') === (typeof blob === 'string'))
|
|
123
|
+
return { kind: 'marker', text: EMBEDDED_RESOURCE_UNREADABLE_TEXT };
|
|
124
|
+
if (typeof text === 'string')
|
|
125
|
+
return { kind: 'block', block: { type: 'text', text } };
|
|
126
|
+
const data = blob;
|
|
127
|
+
const type = typeof mimeType === 'string' ? mimeType : '';
|
|
128
|
+
// image/* or audio/*, with a subtype: a bare `image` is no media type a
|
|
129
|
+
// block can carry, so it reads as any other type does.
|
|
130
|
+
const family = /^(image|audio)\/[^/]/i.exec(type)?.[1]?.toLowerCase();
|
|
131
|
+
if (family)
|
|
132
|
+
return { kind: 'block', block: { type: family, data, mimeType: type } };
|
|
133
|
+
if (type === '')
|
|
134
|
+
return { kind: 'marker', text: EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT };
|
|
135
|
+
return { kind: 'marker', text: embeddedBinaryText(type, data) };
|
|
136
|
+
}
|
|
137
|
+
/** A link's classification, its testimony marked as a link's (fromLink). */
|
|
138
|
+
function linkTestimony(c) {
|
|
139
|
+
return c.kind === 'reference' ? { kind: 'reference', testimony: { ...c.testimony, fromLink: true } } : c;
|
|
140
|
+
}
|
|
93
141
|
function parseReferenceFields(block) {
|
|
94
142
|
if (typeof block.uri !== 'string' || block.uri.length === 0) {
|
|
95
143
|
return { kind: 'invalid', shown: REFERENCE_NO_LINK_TEXT };
|
|
@@ -147,6 +195,29 @@ export const AUDIO_NO_MEDIA_TYPE_TEXT = '[Audio not shown: its data arrived with
|
|
|
147
195
|
* inline data, so a plain link isn't enough either. (A tool result's audio
|
|
148
196
|
* with a link is a reference, as any block with one is there.) */
|
|
149
197
|
export const AUDIO_NO_DATA_TEXT = '[Audio not shown: it arrived with no usable inline data.]';
|
|
198
|
+
/** MCP's embedded resource whose contents are neither text nor a blob, or
|
|
199
|
+
* both. */
|
|
200
|
+
export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = '[Resource not shown: its contents arrived as neither text nor data, or as both.]';
|
|
201
|
+
/** MCP's embedded resource whose blob has no media type, so it can't be
|
|
202
|
+
* shown as an image or audio. */
|
|
203
|
+
export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = '[Resource not shown: its data arrived without a media type.]';
|
|
204
|
+
/** MCP's embedded resource whose blob is of a type no lane can show: what
|
|
205
|
+
* it was, by its type and size. Made well formed (a lone UTF-16 surrogate
|
|
206
|
+
* becomes U+FFFD), as a stub is (#695), since it names the block's media
|
|
207
|
+
* type, which sanitizeLabel leaves as it is: every lane then shows the same
|
|
208
|
+
* text. */
|
|
209
|
+
export function embeddedBinaryText(mimeType, blob) {
|
|
210
|
+
const what = [referenceTypeLabel(mimeType), formatSizeBytes(Math.floor(blob.length * 3 / 4))].filter(Boolean).join(', ');
|
|
211
|
+
return `[Resource not shown: this host can't display its data (${what}).]`.toWellFormed();
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* What the converter (convertBlock, on every lane) shows for a text
|
|
215
|
+
* block whose text isn't a string, which only a nonconformant sender writes,
|
|
216
|
+
* so the agent learns that a block was there. It is the host's word, not the
|
|
217
|
+
* sender's, so it never makes content visible (isVisiblyEmptyConversion in
|
|
218
|
+
* visible-content.ts, which re-exports it).
|
|
219
|
+
*/
|
|
220
|
+
export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
|
|
150
221
|
export const REFERENCE_NO_LINK_TEXT = '[Reference not shown: it arrived without a link.]';
|
|
151
222
|
export const REFERENCE_LINK_TOO_LONG_TEXT = '[Reference not shown: its link is longer than this host accepts.]';
|
|
152
223
|
const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this host can't read.]";
|
|
@@ -195,21 +266,313 @@ export const EXPIRY_REASON = {
|
|
|
195
266
|
* the fetcher's refusal and save_recent_image's all say it: the fetcher dials
|
|
196
267
|
* only through a server that delivered the reference. */
|
|
197
268
|
export const UNBOUND_REASON = "not tied to any server, so there's nowhere to fetch it from";
|
|
198
|
-
/** The
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* configs. */
|
|
269
|
+
/** The refusal for a server the host has no address to dial, one it doesn't
|
|
270
|
+
* connect to over a network, such as a stdio server, as a clause that
|
|
271
|
+
* completes "couldn't fetch <ref>: " and says so of the server: the fetcher
|
|
272
|
+
* dials only through a server's address. A reference's stub, fetch_reference
|
|
273
|
+
* and save_recent_image all give it (dialCheck). */
|
|
204
274
|
export function noAddressReason(serverId) {
|
|
205
275
|
return `this host doesn't connect to the "${serverId}" server over a network, so there's nowhere to fetch it from`;
|
|
206
276
|
}
|
|
207
|
-
/**
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
277
|
+
/** A server's id as the agent reads it in a stub or a refusal: as a label
|
|
278
|
+
* is, bounded and with its controls removed. */
|
|
279
|
+
export function shownServerId(serverId) {
|
|
280
|
+
return sanitizeLabel(serverId, STUB_FIELD_CHARS);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Query and fragment parameters that carry a credential, by name, compared
|
|
284
|
+
* by their letters and digits alone, without letter case, so `access_token`,
|
|
285
|
+
* `accessToken`, `access-token` and `access_token[]` are one name. RFC-005 §5: "Signed URLs and query capabilities are
|
|
286
|
+
* bearer credentials that look like locations", so a uri is shown only where
|
|
287
|
+
* the host's policy classifies it as non-capability-bearing, which here is
|
|
288
|
+
* one with no userinfo and none of these parameters, nor any uri in a
|
|
289
|
+
* parameter's value that has them (uriMayCarryCredential).
|
|
290
|
+
* For example:
|
|
291
|
+
* - AWS's signed URLs: `https://b.s3.amazonaws.com/k?X-Amz-Algorithm=…&X-Amz-Credential=…&X-Amz-Security-Token=…&X-Amz-Signature=…`
|
|
292
|
+
* - Google Cloud Storage's: `https://storage.googleapis.com/b/k?X-Goog-Algorithm=…&X-Goog-Credential=…&X-Goog-Signature=…`
|
|
293
|
+
* - Azure's shared access signatures: `https://a.blob.core.windows.net/c/b?sv=…&se=…&sp=r&sig=…`
|
|
294
|
+
* - CloudFront's: `https://d1.cloudfront.net/k?Expires=…&Signature=…&Key-Pair-Id=…`, or `?Policy=…&Signature=…&Key-Pair-Id=…`
|
|
295
|
+
* - Discord's CDN: `https://cdn.discordapp.com/attachments/1/2/f.png?ex=…&is=…&hm=…`
|
|
296
|
+
* - Backblaze B2's: `https://f000.backblazeb2.com/file/b/k?Authorization=…`
|
|
297
|
+
* - OAuth's: `#access_token=…&token_type=bearer`, `#id_token=…`, `?refresh_token=…`, `?client_secret=…`,
|
|
298
|
+
* and OAuth 1.0a's `?oauth_token=…&oauth_signature=…`
|
|
299
|
+
* - the generic: `?token=…`, `?auth=…`, `?auth_token=…`, `?key=…`, `?apikey=…`, `?api_key=…`, `?api_token=…`,
|
|
300
|
+
* `?x-api-key=…`, `?private_token=…`, `?secret=…`, `?password=…`, `?signature=…`
|
|
301
|
+
* - a session's id, a bearer credential too: `?session_id=…`, `?jsessionid=…` (in a path, as
|
|
302
|
+
* `;jsessionid=…` usually rides, it isn't read)
|
|
303
|
+
* A credential named otherwise, or carried in a path, such as a webhook's
|
|
304
|
+
* `https://discord.com/api/webhooks/<id>/<token>`, isn't caught.
|
|
305
|
+
*/
|
|
306
|
+
const CREDENTIAL_PARAMETERS = new Set([
|
|
307
|
+
'x-amz-signature', 'x-amz-credential', 'x-amz-security-token',
|
|
308
|
+
'x-goog-signature', 'x-goog-credential',
|
|
309
|
+
'sig',
|
|
310
|
+
'signature', 'key-pair-id', 'policy',
|
|
311
|
+
'hm',
|
|
312
|
+
'authorization',
|
|
313
|
+
'access_token', 'id_token', 'refresh_token', 'client_secret',
|
|
314
|
+
'oauth_token', 'oauth_signature',
|
|
315
|
+
'token', 'auth', 'auth_token', 'key', 'api_key', 'api_token', 'x-api-key', 'private_token', 'secret', 'password',
|
|
316
|
+
'session_id', 'jsessionid',
|
|
317
|
+
].map(credentialName));
|
|
318
|
+
/** A parameter's name as CREDENTIAL_PARAMETERS compares it: its letters and
|
|
319
|
+
* digits alone, in lower case. */
|
|
320
|
+
function credentialName(name) {
|
|
321
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Whether a uri may carry a credential: userinfo, or a query or fragment
|
|
325
|
+
* parameter named on CREDENTIAL_PARAMETERS with a value, a fragment read as
|
|
326
|
+
* `name=value&…` as a query is. A name with no value carries nothing, as an
|
|
327
|
+
* anchor (`#token`) or `?token=` doesn't. A `?` or `;` inside either may
|
|
328
|
+
* separate parameters too, as in a route kept in a fragment
|
|
329
|
+
* (`#/reset?token=…`), so the parameters are read both with and without them
|
|
330
|
+
* as separators, and one with a value in either reading counts. A
|
|
331
|
+
* parameter's value that is itself a uri is read the same way
|
|
332
|
+
* (nestedMayCarry): an absolute one, as an image proxy's `?url=<a
|
|
333
|
+
* percent-encoded signed URL>` is once decoded; a protocol-relative one, or
|
|
334
|
+
* a relative one with a query or fragment, as a login's
|
|
335
|
+
* `?next=%2Freset%3Ftoken%3D…` is; and one encoded again inside the value,
|
|
336
|
+
* as a redirector that encodes an already encoded url leaves it, each read
|
|
337
|
+
* and each decode a level of its own. That goes up to CREDENTIAL_NESTING
|
|
338
|
+
* levels deep, and a uri reached deeper may carry one, since a bound that
|
|
339
|
+
* let the next level through would be the same gap one level down. A uri
|
|
340
|
+
* carried in another encoding, such as base64, isn't read. A uri that
|
|
341
|
+
* doesn't parse, even as a relative reference, may, since its parts can't be
|
|
342
|
+
* read.
|
|
343
|
+
*/
|
|
344
|
+
export function uriMayCarryCredential(uri) {
|
|
345
|
+
let u;
|
|
346
|
+
try {
|
|
347
|
+
// Absolute first: against an http base, `http:user:pass@host` would read
|
|
348
|
+
// as a relative path and hide its userinfo.
|
|
349
|
+
u = new URL(uri);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
try {
|
|
353
|
+
u = new URL(uri, RELATIVE_BASE);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return urlMayCarryCredential(u, 0);
|
|
360
|
+
}
|
|
361
|
+
/** The base a relative uri is read against: it supplies only a scheme and a
|
|
362
|
+
* path, and a credential lives in the uri's own authority, query or
|
|
363
|
+
* fragment. */
|
|
364
|
+
const RELATIVE_BASE = 'http://relative.invalid/';
|
|
365
|
+
/** How deep uriMayCarryCredential reads uris nested in parameters' values,
|
|
366
|
+
* each one a parameter's value in the uri before it. */
|
|
367
|
+
const CREDENTIAL_NESTING = 4;
|
|
368
|
+
function urlMayCarryCredential(u, depth) {
|
|
369
|
+
if (u.username !== '' || u.password !== '')
|
|
370
|
+
return true;
|
|
371
|
+
// Read both ways: with '&' alone separating parameters, as a server
|
|
372
|
+
// reading the query as WHATWG does sees them, and with '?' and ';' too, so
|
|
373
|
+
// a value that opens with one (`token=;…`) still counts in the first.
|
|
374
|
+
const named = (params) => [params, params.replace(/[?;]/g, '&')].some((read) => [...new URLSearchParams(read).entries()].some(([name, value]) => value !== ''
|
|
375
|
+
&& (CREDENTIAL_PARAMETERS.has(credentialName(name)) || nestedMayCarry(value, depth))));
|
|
376
|
+
return named(u.search) || named(u.hash.slice(1));
|
|
377
|
+
}
|
|
378
|
+
/** Whether a parameter's value is a uri that may carry a credential, read in
|
|
379
|
+
* this order: as an absolute uri; else, where its form is a relative
|
|
380
|
+
* reference's (it starts with `/` or `\`, or holds `?` or `#`), resolved
|
|
381
|
+
* against RELATIVE_BASE, never against the uri it sits in, which may have
|
|
382
|
+
* no path to resolve against, as a `mailto:` hasn't; else, where decoding
|
|
383
|
+
* it once more changes it, decoded and read again from the start. Each read
|
|
384
|
+
* or decode is a level, and a decode is lenient, as the parameter's own
|
|
385
|
+
* was, so a stray `%` doesn't stop it. A value that is none of these, such
|
|
386
|
+
* as a word, answers false; within the bound a decode alone never
|
|
387
|
+
* withholds, and past it a value that would still decode does. */
|
|
388
|
+
function nestedMayCarry(value, depth) {
|
|
389
|
+
let inner = null;
|
|
390
|
+
try {
|
|
391
|
+
inner = new URL(value);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Its form as the URL parser reads it, which first trims C0 controls and
|
|
395
|
+
// spaces, so `\t//user:pass@…` is the protocol-relative uri it resolves
|
|
396
|
+
// as. The tabs and newlines it also drops can't change this form once the
|
|
397
|
+
// lead is trimmed: none is a `/`, `\\`, `?` or `#`.
|
|
398
|
+
if (/^[\u0000-\u0020]*[/\\]|[?#]/.test(value)) {
|
|
399
|
+
try {
|
|
400
|
+
inner = new URL(value, RELATIVE_BASE);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// Against a hierarchical base only a value that is protocol-relative
|
|
404
|
+
// as the parser reads it (`//`, or `\\` as a special scheme reads it)
|
|
405
|
+
// fails to resolve, on its authority, and it may carry one, as a uri
|
|
406
|
+
// that doesn't parse may: its authority can't be read. So an
|
|
407
|
+
// ordinary value such as `q=// note` is withheld too.
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (inner !== null)
|
|
413
|
+
return depth >= CREDENTIAL_NESTING || urlMayCarryCredential(inner, depth + 1);
|
|
414
|
+
// Decoded as the parameter itself was, leniently: a stray `%` stays as it
|
|
415
|
+
// is rather than stopping the reading, as it doesn't stop a server's.
|
|
416
|
+
// `+` and `&` are escaped first, so they come back as themselves.
|
|
417
|
+
const decoded = new URLSearchParams(`v=${value.replace(/[+&]/g, encodeURIComponent)}`).get('v') ?? value;
|
|
418
|
+
if (decoded === value)
|
|
419
|
+
return false;
|
|
420
|
+
// Past the bound, a value that would still decode is withheld: the check's
|
|
421
|
+
// work stops where its reading does, and a uri found deeper would be.
|
|
422
|
+
return depth >= CREDENTIAL_NESTING || nestedMayCarry(decoded, depth + 1);
|
|
423
|
+
}
|
|
424
|
+
// What calling again can do, for the refusals a reference's stub shares with
|
|
425
|
+
// a fetch; the fetcher has these and its own. None names a tool: a fetch
|
|
426
|
+
// doesn't know where its reference came from.
|
|
427
|
+
export const NOTHING_TO_DO = "Calling again won't change that.";
|
|
428
|
+
/** The registry keys a record by its uri, so a fresh reference to the same
|
|
429
|
+
* uri arrives as this very id and refreshes its record. */
|
|
430
|
+
export const FRESH_REFERENCE = "Once a fresh reference to it arrives, fetching may work; until then, fetching it again won't.";
|
|
431
|
+
/** A server's address is read from the host's configs as they are at each
|
|
432
|
+
* fetch. */
|
|
433
|
+
export const SERVER_BACK = "Calling again won't change that unless whoever runs this host adds that server back.";
|
|
434
|
+
const SERVER_ADDRESS = "Calling again won't change that unless whoever runs this host corrects the server's address.";
|
|
435
|
+
/** ws→http / wss→https with default ports elided, for origin comparison. */
|
|
436
|
+
export function normalizedOrigin(url) {
|
|
437
|
+
try {
|
|
438
|
+
const u = new URL(url);
|
|
439
|
+
const scheme = u.protocol === 'ws:' ? 'http:' : u.protocol === 'wss:' ? 'https:' : u.protocol;
|
|
440
|
+
const defaultPort = scheme === 'https:' ? '443' : '80';
|
|
441
|
+
const port = u.port === '' || u.port === defaultPort ? '' : `:${u.port}`;
|
|
442
|
+
return `${scheme}//${u.hostname.toLowerCase()}${port}`;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** A server's address as it's configured, its scheme and host, for a
|
|
449
|
+
* clause: never its userinfo, path or query. URL's own origin is 'null' for
|
|
450
|
+
* a scheme that isn't special, which normalizedOrigin accepts. The caller
|
|
451
|
+
* has already parsed it. */
|
|
452
|
+
function configuredOrigin(url) {
|
|
453
|
+
const u = new URL(url);
|
|
454
|
+
return `${u.protocol}//${u.host}`;
|
|
455
|
+
}
|
|
456
|
+
/** The §6.1/§7.1 policy check of a uri against the url a server is dialed
|
|
457
|
+
* at: null where a fetch through that server may fetch it. */
|
|
458
|
+
function originRefusal(uri, serverUrl, serverId) {
|
|
459
|
+
const refOrigin = normalizedOrigin(uri);
|
|
460
|
+
if (!refOrigin)
|
|
461
|
+
return { error: "its address doesn't parse as a URL", remedy: NOTHING_TO_DO };
|
|
462
|
+
// A server's address that doesn't parse leaves nothing to dial. The
|
|
463
|
+
// framework's resolver gives only an address its transport dials, so this
|
|
464
|
+
// guards other resolvers.
|
|
465
|
+
const dialedOrigin = normalizedOrigin(serverUrl);
|
|
466
|
+
if (!dialedOrigin) {
|
|
467
|
+
return {
|
|
468
|
+
error: `the "${shownServerId(serverId)}" server's address doesn't parse as a URL, so there's nowhere to fetch it from`,
|
|
469
|
+
remedy: SERVER_ADDRESS,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const refUrl = new URL(uri);
|
|
473
|
+
// A user name or password in the uri is a credential embedded in it,
|
|
474
|
+
// which RFC-005 §6.1 forbids a server to mint and lets a host refuse
|
|
475
|
+
// (vector 6). This host neither sends nor repeats it: fetch refuses such
|
|
476
|
+
// a url with a message that holds it whole.
|
|
477
|
+
if (refUrl.username !== '' || refUrl.password !== '') {
|
|
478
|
+
return { error: "its address carries a user name or password, which this host doesn't send", remedy: NOTHING_TO_DO };
|
|
479
|
+
}
|
|
480
|
+
// The uri's own scheme, not its origin's: an origin reads ws: as http:
|
|
481
|
+
// and wss: as https:, and fetch refuses a ws: or wss: uri ('unknown
|
|
482
|
+
// scheme') before connecting, which would read as a failed request a
|
|
483
|
+
// later try can mend.
|
|
484
|
+
const scheme = refUrl.protocol;
|
|
485
|
+
if (scheme !== 'https:' && scheme !== 'http:') {
|
|
486
|
+
return {
|
|
487
|
+
error: `its address uses ${sanitizeLabel(scheme, STUB_FIELD_CHARS)}, and this host fetches only http: and https: addresses`,
|
|
488
|
+
remedy: NOTHING_TO_DO,
|
|
489
|
+
inStub: 'scheme not allowed: this host fetches only http and https uris',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Origins compare with their schemes, so this also refuses an http:
|
|
493
|
+
// reference from a server dialed over TLS (wss://).
|
|
494
|
+
if (refOrigin !== dialedOrigin) {
|
|
495
|
+
return {
|
|
496
|
+
error: `it points to ${refOrigin}, not to the "${shownServerId(serverId)}" server's own address (${configuredOrigin(serverUrl)}), ` +
|
|
497
|
+
'and this host fetches only from the server itself',
|
|
498
|
+
remedy: NOTHING_TO_DO,
|
|
499
|
+
inStub: "third-party origin: it isn't on its server's own address, so this host doesn't fetch it",
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The server a fetch of a reference would dial: its servers, in the order
|
|
506
|
+
* they delivered it, the first whose address is on the uri's origin (§6.2).
|
|
507
|
+
* When none is, the first server's refusal (unknown to this host, with no
|
|
508
|
+
* address, or the origin check's), and for a record no server delivered,
|
|
509
|
+
* that it's unbound, which a later delivery of the uri by a server mends.
|
|
510
|
+
*/
|
|
511
|
+
export function dialServer(record, addressOf) {
|
|
512
|
+
let refusal;
|
|
513
|
+
let dialed;
|
|
514
|
+
for (const serverId of record.serverIds) {
|
|
515
|
+
const address = addressOf(serverId);
|
|
516
|
+
if (address === undefined) {
|
|
517
|
+
refusal ??= { error: `this host no longer has the "${shownServerId(serverId)}" server it came from`, remedy: SERVER_BACK };
|
|
518
|
+
}
|
|
519
|
+
else if (address === 'no-address') {
|
|
520
|
+
refusal ??= { error: noAddressReason(shownServerId(serverId)), remedy: NOTHING_TO_DO };
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
const offOrigin = originRefusal(record.testimony.uri, address.url, serverId);
|
|
524
|
+
if (offOrigin === null) {
|
|
525
|
+
dialed = serverId;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
refusal ??= offOrigin;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return dialed === undefined
|
|
532
|
+
? { refusal: refusal ?? { error: `it's ${UNBOUND_REASON}`, remedy: FRESH_REFERENCE } }
|
|
533
|
+
: { serverId: dialed };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Where a fetch of a reference would dial, or why it would be refused before
|
|
537
|
+
* dialing, whatever the fetch: dialServer's server, then the reference's
|
|
538
|
+
* expiry. A pure function of the record and the servers' addresses: a
|
|
539
|
+
* reference's stub asks it of the registry's (unfetchableReason), and the
|
|
540
|
+
* fetcher's pre-dial check of its own framework's configs, whose credential
|
|
541
|
+
* goes with the address it checks. In a process with one running framework,
|
|
542
|
+
* or with running frameworks that agree on each id they share, the two
|
|
543
|
+
* sources hold the same addresses, so a stub promises the server the fetch
|
|
544
|
+
* dials. A stub reads an expiry with its result's claims, and the fetch with
|
|
545
|
+
* the record's latest testimony, so where one result names a uri both
|
|
546
|
+
* expired and fresh, its stub says expired while a fetch can succeed. A
|
|
547
|
+
* stopped framework's addresses are out of the registry, so its stubs read
|
|
548
|
+
* another running framework's entry for the same id where there is one, and
|
|
549
|
+
* otherwise say the server is unknown, while its configs still resolve it.
|
|
550
|
+
* Where two
|
|
551
|
+
* frameworks configure one id differently, only a stub's words can be wrong,
|
|
552
|
+
* never where a token goes. A fetch's own limits, its budget and ceiling,
|
|
553
|
+
* come after it.
|
|
554
|
+
*/
|
|
555
|
+
export function dialCheck(record, addressOf, nowMs = Date.now(),
|
|
556
|
+
/** For a stub or a script's object, the claims of the result it's shown
|
|
557
|
+
* in, which decide where its references name the record's uri, the
|
|
558
|
+
* record's own testimony deciding otherwise (claimedExpiry). A fetch
|
|
559
|
+
* reads the record alone. */
|
|
560
|
+
claims) {
|
|
561
|
+
const server = dialServer(record, addressOf);
|
|
562
|
+
if ('refusal' in server)
|
|
563
|
+
return server;
|
|
564
|
+
const expiry = claimedExpiry(record, claims, nowMs);
|
|
565
|
+
return expiry ? { refusal: { error: `the reference is ${EXPIRY_REASON[expiry]}`, remedy: FRESH_REFERENCE } } : server;
|
|
566
|
+
}
|
|
567
|
+
/** Strip control characters (C0, DEL, C1), every bidi control (marks,
|
|
568
|
+
* embeddings, overrides, isolates, and the Arabic letter mark) and the line
|
|
569
|
+
* and paragraph separators (U+2028, U+2029), which a regex reads as line
|
|
570
|
+
* ends, so a label can't start a line of its own, such as one the image
|
|
571
|
+
* ledger would read as a slot. Then trim the whitespace (a BOM included)
|
|
572
|
+
* left at either end, and bound the length. Labels are labels — storage
|
|
573
|
+
* naming is host-generated elsewhere (§7.3). */
|
|
211
574
|
export function sanitizeLabel(s, maxChars) {
|
|
212
|
-
const cleaned = s.replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
|
|
575
|
+
const cleaned = s.replace(/[\p{Cc}\p{Bidi_Control}\u2028\u2029]/gu, '').trim();
|
|
213
576
|
if (cleaned.length <= maxChars)
|
|
214
577
|
return cleaned;
|
|
215
578
|
let cut = cleaned.slice(0, Math.max(0, maxChars - 1));
|
|
@@ -244,7 +607,7 @@ const MAX_RECORDS = 2000;
|
|
|
244
607
|
export class ReferenceRegistry {
|
|
245
608
|
records = new Map();
|
|
246
609
|
/** uri → refId. The uri ALONE is the identity key: a tool result's stubs
|
|
247
|
-
* (tool-result-history.ts's,
|
|
610
|
+
* (tool-result-history.ts's, which both copies of a tool result take)
|
|
248
611
|
* and a script's demand fetch register with no server context,
|
|
249
612
|
* while the registrations that know the server carry its id: a tool
|
|
250
613
|
* result's, before it's shown, whether the agent's dispatch or an
|
|
@@ -261,7 +624,49 @@ export class ReferenceRegistry {
|
|
|
261
624
|
* its own origin, for a uri it delivered. */
|
|
262
625
|
byKey = new Map();
|
|
263
626
|
counter = 0;
|
|
627
|
+
/** Each open journal's copies (openJournal), by uri. */
|
|
628
|
+
journals = new Set();
|
|
629
|
+
/**
|
|
630
|
+
* Keep, until `close`, each record as it stood when this opened: a copy
|
|
631
|
+
* taken the first time a registration names its uri, whatever it changes,
|
|
632
|
+
* or evicts its record, or null where the uri had none then. `before(uri)`
|
|
633
|
+
* gives that copy, or, where no registration has named the uri since, the
|
|
634
|
+
* record as it is, which is the same. A fetch that saves a copy while it's
|
|
635
|
+
* open sets its record's saved path without registering: a record no
|
|
636
|
+
* registration has named since is read with it, and one a registration
|
|
637
|
+
* named first is read as it stood then, saved or not. For a record a fetch
|
|
638
|
+
* saved meanwhile, both readings are of one the host can fetch, since a
|
|
639
|
+
* fetch saves only what passed the checks a link's line reads too, except
|
|
640
|
+
* where two frameworks configure one server id differently, which a stub's
|
|
641
|
+
* words can't tell apart either (setServerAddress). Each
|
|
642
|
+
* open has its own copies, so lanes that overlap keep theirs apart, and
|
|
643
|
+
* closing one lets its copies go.
|
|
644
|
+
*/
|
|
645
|
+
openJournal() {
|
|
646
|
+
const copies = new Map();
|
|
647
|
+
this.journals.add(copies);
|
|
648
|
+
return {
|
|
649
|
+
before: (uri) => (copies.has(uri) ? copies.get(uri) ?? undefined : this.findByUri(uri)),
|
|
650
|
+
close: () => { this.journals.delete(copies); },
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/** Copy a uri's record into each open journal that has none for it yet,
|
|
654
|
+
* a copy of its own for each. */
|
|
655
|
+
keepFirst(uri) {
|
|
656
|
+
if (this.journals.size === 0)
|
|
657
|
+
return;
|
|
658
|
+
const record = this.findByUri(uri);
|
|
659
|
+
for (const copies of this.journals) {
|
|
660
|
+
if (!copies.has(uri))
|
|
661
|
+
copies.set(uri, record ? { ...record, serverIds: [...record.serverIds] } : null);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/** How many journals are open: none once every lane has closed its own. */
|
|
665
|
+
get journalCount() {
|
|
666
|
+
return this.journals.size;
|
|
667
|
+
}
|
|
264
668
|
register(testimony, serverId) {
|
|
669
|
+
this.keepFirst(testimony.uri);
|
|
265
670
|
const existingId = this.byKey.get(testimony.uri);
|
|
266
671
|
if (existingId !== undefined) {
|
|
267
672
|
const existing = this.records.get(existingId);
|
|
@@ -272,8 +677,9 @@ export class ReferenceRegistry {
|
|
|
272
677
|
// Refresh testimony while unfetched: a re-issued reference with a new
|
|
273
678
|
// digest/expiry supersedes the stale claims. Once fetched, the record
|
|
274
679
|
// describes verified bytes on disk and keeps the testimony they were
|
|
275
|
-
// verified against.
|
|
276
|
-
|
|
680
|
+
// verified against. A link claims no digest, expiry or disposition,
|
|
681
|
+
// so its testimony never replaces a record's claims.
|
|
682
|
+
if (!existing.fetchedPath && !testimony.fromLink)
|
|
277
683
|
existing.testimony = testimony;
|
|
278
684
|
// Newest again, so a uri just mentioned is the last to be evicted.
|
|
279
685
|
this.records.delete(existingId);
|
|
@@ -289,6 +695,8 @@ export class ReferenceRegistry {
|
|
|
289
695
|
const oldest = this.records.keys().next().value;
|
|
290
696
|
if (oldest !== undefined) {
|
|
291
697
|
const old = this.records.get(oldest);
|
|
698
|
+
if (old)
|
|
699
|
+
this.keepFirst(old.testimony.uri);
|
|
292
700
|
this.records.delete(oldest);
|
|
293
701
|
if (old)
|
|
294
702
|
this.byKey.delete(old.testimony.uri);
|
|
@@ -299,6 +707,36 @@ export class ReferenceRegistry {
|
|
|
299
707
|
get(refId) {
|
|
300
708
|
return this.records.get(refId);
|
|
301
709
|
}
|
|
710
|
+
/** How the host reaches each server, by id, as each framework in the process
|
|
711
|
+
* configured it, which a reference's stub reads (unfetchableReason). A
|
|
712
|
+
* fetch reads its own framework's configs instead, so a server's token only
|
|
713
|
+
* ever goes to the origin its own config names. Keyed by id as a record's
|
|
714
|
+
* servers are: where two frameworks configure one id, the latest entry is
|
|
715
|
+
* in force, and a stub's words can be wrong for the other one's fetch. */
|
|
716
|
+
addresses = new Map();
|
|
717
|
+
/** Record how the host reaches a server, as a framework (`owner`) does for
|
|
718
|
+
* each server it configures. Its entry for the id is then in force. */
|
|
719
|
+
setServerAddress(serverId, address, owner) {
|
|
720
|
+
const entries = (this.addresses.get(serverId) ?? []).filter((e) => e.owner !== owner);
|
|
721
|
+
entries.push({ owner, address });
|
|
722
|
+
this.addresses.set(serverId, entries);
|
|
723
|
+
}
|
|
724
|
+
/** Take back a framework's entry for a server, as it does for a server it
|
|
725
|
+
* removes, and for each of its servers when it stops. Another framework's
|
|
726
|
+
* entry for the id, if any, is in force again; with none, a stub reads the
|
|
727
|
+
* id's references as an unknown server's. After a removal, that framework's
|
|
728
|
+
* fetch refuses them too. A stopped framework keeps its configs, so its
|
|
729
|
+
* fetcher, called before it starts again, can still dial them. */
|
|
730
|
+
forgetServerAddress(serverId, owner) {
|
|
731
|
+
const entries = (this.addresses.get(serverId) ?? []).filter((e) => e.owner !== owner);
|
|
732
|
+
if (entries.length > 0)
|
|
733
|
+
this.addresses.set(serverId, entries);
|
|
734
|
+
else
|
|
735
|
+
this.addresses.delete(serverId);
|
|
736
|
+
}
|
|
737
|
+
serverAddress(serverId) {
|
|
738
|
+
return this.addresses.get(serverId)?.at(-1)?.address;
|
|
739
|
+
}
|
|
302
740
|
/** Find the record for a uri. */
|
|
303
741
|
findByUri(uri) {
|
|
304
742
|
const id = this.byKey.get(uri);
|
|
@@ -315,13 +753,15 @@ export function referenceTypeLabel(mimeType) {
|
|
|
315
753
|
return mimeType ? sanitizeLabel(mimeType, STUB_FIELD_CHARS) : '';
|
|
316
754
|
}
|
|
317
755
|
/**
|
|
318
|
-
* The longest
|
|
756
|
+
* The longest stub buildReferenceStub writes, in UTF-16 code units, from its
|
|
319
757
|
* own bounds: the longest id the registry mints, then the name, the type with
|
|
320
758
|
* the largest size it shows, and the origin, each at its bound, and the
|
|
321
759
|
* longest ending, a saved copy's path, with their separators. The other
|
|
322
760
|
* endings, the fetch hint and the reasons a fetch would be refused, are
|
|
323
761
|
* shorter. A line shaped like a stub but longer than this is quoted text the
|
|
324
|
-
* writer can't have written, and no image slot (tool-image-ledger.ts).
|
|
762
|
+
* writer can't have written, and no image slot (tool-image-ledger.ts). A
|
|
763
|
+
* tool result's link line (StubOptions.unfetchedLinkUri) isn't shaped like a
|
|
764
|
+
* stub, so it's no slot whatever its length.
|
|
325
765
|
*/
|
|
326
766
|
export const REFERENCE_STUB_MAX_CHARS = `[ref_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
|
|
327
767
|
+ STUB_FIELD_CHARS
|
|
@@ -329,14 +769,166 @@ export const REFERENCE_STUB_MAX_CHARS = `[ref_${MAX_SAFE.toString(36)}_] `.lengt
|
|
|
329
769
|
+ ' — '.length + STUB_FIELD_CHARS
|
|
330
770
|
+ ' — '.length + 'saved: '.length + SAVED_PATH_CHARS;
|
|
331
771
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
772
|
+
* A uri as a link shows it: cleaned as sanitizeLabel cleans a label (controls
|
|
773
|
+
* and bidi marks dropped, surrounding space trimmed; a uri past
|
|
774
|
+
* REFERENCE_LIMITS.uri is no reference, so none is shortened), and well
|
|
775
|
+
* formed, as every copy is before it's shown, a lone surrogate showing as
|
|
776
|
+
* U+FFFD. A `never` is about what a link shows, so it's found by this text
|
|
777
|
+
* (neverShown): a claim of `never` on a uri withholds every link that shows
|
|
778
|
+
* the same text, two uris that clean alike sharing it. Every other claim is about the record,
|
|
779
|
+
* which the fetch reads by its raw uri, so it's kept by the raw uri (Ada-1017,
|
|
780
|
+
* room-203 #202239).
|
|
337
781
|
*/
|
|
338
|
-
export function
|
|
782
|
+
export function shownUri(uri) {
|
|
783
|
+
return sanitizeLabel(uri, REFERENCE_LIMITS.uri).toWellFormed();
|
|
784
|
+
}
|
|
785
|
+
const NEVER_SHOWN = new WeakMap();
|
|
786
|
+
/** The texts a link shows (shownUri) for the uris a result's claims mark
|
|
787
|
+
* `never`, read once per claims. A ResultClaims isn't changed once made. */
|
|
788
|
+
function neverShown(claims) {
|
|
789
|
+
let shown = NEVER_SHOWN.get(claims);
|
|
790
|
+
if (!shown) {
|
|
791
|
+
shown = new Set([...claims].filter(([, c]) => c.never).map(([uri]) => shownUri(uri)));
|
|
792
|
+
NEVER_SHOWN.set(claims, shown);
|
|
793
|
+
}
|
|
794
|
+
return shown;
|
|
795
|
+
}
|
|
796
|
+
/** A result's claims (ResultClaims), from the testimonies of its references,
|
|
797
|
+
* each read once, as referenceExpiry reads an expiry. */
|
|
798
|
+
export function resultClaims(testimonies, nevers = []) {
|
|
799
|
+
const claims = new Map();
|
|
800
|
+
// A uri inline content marks `disposition: never` (inlineNeverUri), inline
|
|
801
|
+
// data beside its data, the classifier's contradiction, or an embedded
|
|
802
|
+
// resource: withheld unconditionally (RFC-005 §5), though the block is no
|
|
803
|
+
// reference. Only its never counts, since its other claims could loosen
|
|
804
|
+
// (Ada-1017, room-203 #201013, #206688).
|
|
805
|
+
const entry = (uri) => {
|
|
806
|
+
let c = claims.get(uri);
|
|
807
|
+
if (!c)
|
|
808
|
+
claims.set(uri, c = { never: false, rejected: false, unparseable: false, earliestMs: null });
|
|
809
|
+
return c;
|
|
810
|
+
};
|
|
811
|
+
for (const uri of nevers)
|
|
812
|
+
entry(uri).never = true;
|
|
813
|
+
for (const t of testimonies) {
|
|
814
|
+
if (t.fromLink)
|
|
815
|
+
continue;
|
|
816
|
+
const c = entry(t.uri);
|
|
817
|
+
if (t.disposition === 'never')
|
|
818
|
+
c.never = true;
|
|
819
|
+
if (t.rejectedFields.includes('expiresAt')) {
|
|
820
|
+
c.rejected = true;
|
|
821
|
+
}
|
|
822
|
+
else if (t.expiresAt !== undefined) {
|
|
823
|
+
const ms = Date.parse(t.expiresAt);
|
|
824
|
+
if (Number.isNaN(ms))
|
|
825
|
+
c.unparseable = true;
|
|
826
|
+
else if (c.earliestMs === null || ms < c.earliestMs)
|
|
827
|
+
c.earliestMs = ms;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return claims;
|
|
831
|
+
}
|
|
832
|
+
/** The claims of a content array's references, read from its elements as
|
|
833
|
+
* the array shows them: an element's own fields, such as an image's
|
|
834
|
+
* `_meta`, aren't shown, so a reference inside them doesn't count. */
|
|
835
|
+
export function contentClaims(blocks) {
|
|
836
|
+
const testimonies = [];
|
|
837
|
+
const nevers = [];
|
|
838
|
+
for (const block of blocks) {
|
|
839
|
+
const c = classifyBlock(block);
|
|
840
|
+
if (c.kind === 'reference')
|
|
841
|
+
testimonies.push(c.testimony);
|
|
842
|
+
const never = inlineNeverUri(block, c);
|
|
843
|
+
if (never !== null)
|
|
844
|
+
nevers.push(never);
|
|
845
|
+
}
|
|
846
|
+
return resultClaims(testimonies, nevers);
|
|
847
|
+
}
|
|
848
|
+
/** The uri inline content marks `disposition: never`, which counts for that
|
|
849
|
+
* uri's claims (resultClaims) though the block is no reference: inline data
|
|
850
|
+
* beside its uri, the classifier's contradiction, and MCP's embedded
|
|
851
|
+
* resource, whose uri sits under `resource` (Ada-1017, room-203 #206688).
|
|
852
|
+
* Null for any other block. A uri that isn't a non-empty string names
|
|
853
|
+
* nothing a claim is kept for. */
|
|
854
|
+
export function inlineNeverUri(block, c = classifyBlock(block)) {
|
|
855
|
+
// Either kind is a record, so its fields can be read.
|
|
856
|
+
if (!(c.kind === 'inline' && c.contradiction) && c.kind !== 'embedded')
|
|
857
|
+
return null;
|
|
858
|
+
const b = block;
|
|
859
|
+
const uri = c.kind === 'embedded' ? b.resource.uri : b.uri;
|
|
860
|
+
return b.disposition === 'never' && typeof uri === 'string' && uri !== '' ? uri : null;
|
|
861
|
+
}
|
|
862
|
+
/** The claims a decision about a record reads: its result's for its uri,
|
|
863
|
+
* where the result's references name it, and otherwise the record's own
|
|
864
|
+
* testimony. */
|
|
865
|
+
function uriClaims(record, claims) {
|
|
866
|
+
return claims?.get(record.testimony.uri) ?? resultClaims([record.testimony]).get(record.testimony.uri);
|
|
867
|
+
}
|
|
868
|
+
/** Whether a record's uri is withheld: marked `disposition: never` by its
|
|
869
|
+
* result's references, for any uri a link shows as the same text
|
|
870
|
+
* (shownUri), or by its own testimony where none of them names it. */
|
|
871
|
+
export function claimedNever(record, claims) {
|
|
872
|
+
if (claims && neverShown(claims).has(shownUri(record.testimony.uri)))
|
|
873
|
+
return true;
|
|
874
|
+
return uriClaims(record, claims)?.never === true;
|
|
875
|
+
}
|
|
876
|
+
/** The expiry a record counts, as of `nowMs`: the first of EXPIRY_REASON's
|
|
877
|
+
* cases its claims give (uriClaims), so the order of the blocks doesn't
|
|
878
|
+
* choose the words. Null when none does. */
|
|
879
|
+
export function claimedExpiry(record, claims, nowMs) {
|
|
880
|
+
const c = uriClaims(record, claims);
|
|
881
|
+
if (!c)
|
|
882
|
+
return null;
|
|
883
|
+
const given = {
|
|
884
|
+
passed: c.earliestMs !== null && c.earliestMs <= nowMs,
|
|
885
|
+
unparseable: c.unparseable,
|
|
886
|
+
rejected: c.rejected,
|
|
887
|
+
};
|
|
888
|
+
return Object.keys(EXPIRY_REASON).find((reason) => given[reason]) ?? null;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* The uri a tool result's link shows, or null where it shows none: a link
|
|
892
|
+
* (fromLink) the host can't fetch, whose uri can't carry a credential, and
|
|
893
|
+
* that no reference withholds with `disposition: never`, keeps its uri, the
|
|
894
|
+
* agent's only handle on it. A `data:` uri holds its content rather than
|
|
895
|
+
* naming where it lies, so it's never shown: a link's line isn't a way for
|
|
896
|
+
* inline data to reach context. A tool result's link line (buildReferenceStub,
|
|
897
|
+
* unfetchedLinkUri) and a script's object for the link
|
|
898
|
+
* (referenceBlockForScript) both ask this, so the model and a script, whose
|
|
899
|
+
* output comes back to the model, see a link's uri alike.
|
|
900
|
+
*
|
|
901
|
+
* The credential check reads the uri as it would be shown, with what
|
|
902
|
+
* sanitizeLabel strips gone, so a uri split by a control character is read
|
|
903
|
+
* as it would read: `ht\u0000tps://user:pass@…` parses as a path, and shows
|
|
904
|
+
* as userinfo. A `disposition: "never"` for the uri keeps it out whatever
|
|
905
|
+
* the link says: any reference's in the same result (claims), for this uri
|
|
906
|
+
* or one shown as the same text, or, where no reference in the result names
|
|
907
|
+
* the uri, the record's.
|
|
908
|
+
*/
|
|
909
|
+
export function shownLinkUri(t, record, nowMs = Date.now(), claims) {
|
|
910
|
+
if (!t.fromLink || claimedNever(record, claims))
|
|
911
|
+
return null;
|
|
912
|
+
if (unfetchableReason(record, nowMs, claims) === null)
|
|
913
|
+
return null;
|
|
914
|
+
const uri = shownUri(t.uri);
|
|
915
|
+
return uriMayCarryCredential(uri) || /^data:/i.test(uri) ? null : uri;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Build the model-visible stub for a reference. Its length is bounded by
|
|
919
|
+
* host constants and independent of every server-supplied field's length:
|
|
920
|
+
* REFERENCE_STUB_MAX_CHARS. Under `disposition:"never"` the uri is omitted
|
|
921
|
+
* unconditionally; under `ref`/absent it is omitted too — this host's default
|
|
922
|
+
* policy is the opaque id everywhere (the safe default of §5), with retrieval
|
|
923
|
+
* via the record. The one exception is a tool result's link the host can't
|
|
924
|
+
* fetch (StubOptions.unfetchedLinkUri), which is no stub but a line of its
|
|
925
|
+
* own, its uri bounded by REFERENCE_LIMITS.uri.
|
|
926
|
+
*/
|
|
927
|
+
export function buildReferenceStub(t, provenance, serverId, options) {
|
|
339
928
|
const record = referenceRegistry.register(t, serverId);
|
|
929
|
+
// One clock reading for the whole stub, so whether it shows a link's uri
|
|
930
|
+
// and the reason it gives can't disagree across an expiry.
|
|
931
|
+
const nowMs = Date.now();
|
|
340
932
|
const parts = [];
|
|
341
933
|
// A label is shown only when something is left of it once sanitized.
|
|
342
934
|
const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
|
|
@@ -359,30 +951,40 @@ export function buildReferenceStub(t, provenance, serverId) {
|
|
|
359
951
|
parts.push(`saved: ${sanitizeLabel(record.fetchedPath, SAVED_PATH_CHARS)}`);
|
|
360
952
|
}
|
|
361
953
|
else {
|
|
362
|
-
// A reference
|
|
363
|
-
//
|
|
364
|
-
|
|
954
|
+
// A reference the fetcher would refuse says why, rather than inviting a
|
|
955
|
+
// fetch_reference call.
|
|
956
|
+
if (options?.unfetchedLinkUri) {
|
|
957
|
+
const uri = shownLinkUri(t, record, nowMs, options.claims);
|
|
958
|
+
if (uri !== null)
|
|
959
|
+
return unfetchedLinkText(t, uri);
|
|
960
|
+
}
|
|
961
|
+
parts.push(unfetchableReason(record, nowMs, options?.claims) ?? 'fetch with fetch_reference');
|
|
365
962
|
}
|
|
366
963
|
return `[${record.refId}] ${parts.join(' — ') || 'referenced content'}`;
|
|
367
964
|
}
|
|
368
965
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
966
|
+
* What a tool result's link the host can't fetch shows, where its uri can't
|
|
967
|
+
* carry a credential: its name, its uri and its type, as a standard MCP
|
|
968
|
+
* server's link shows (StubOptions.unfetchedLinkUri).
|
|
969
|
+
*/
|
|
970
|
+
function unfetchedLinkText(t, uri) {
|
|
971
|
+
const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
|
|
972
|
+
const mimeType = referenceTypeLabel(t.mimeType);
|
|
973
|
+
return `[resource link${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Why the fetcher would refuse a reference before it dials, whatever the
|
|
977
|
+
* fetch, in its stub's words: the registry's dialCheck, from its record and
|
|
978
|
+
* its servers' addresses. Null for a saved copy, which the fetcher serves
|
|
979
|
+
* before any check, and where a fetch would dial. A fetch's own limits, the
|
|
980
|
+
* server's spent budget or a claimed size over the ceiling it's given, the
|
|
981
|
+
* fetcher gives in its own words, and save_recent_image says them too.
|
|
378
982
|
*/
|
|
379
|
-
export function unfetchableReason(record, nowMs = Date.now()) {
|
|
983
|
+
export function unfetchableReason(record, nowMs = Date.now(), claims) {
|
|
380
984
|
if (record.fetchedPath)
|
|
381
985
|
return null;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const expiry = referenceExpiry(record.testimony, nowMs);
|
|
385
|
-
return expiry ? EXPIRY_REASON[expiry] : null;
|
|
986
|
+
const dial = dialCheck(record, (serverId) => referenceRegistry.serverAddress(serverId), nowMs, claims);
|
|
987
|
+
return 'refusal' in dial ? dial.refusal.inStub ?? dial.refusal.error : null;
|
|
386
988
|
}
|
|
387
989
|
/**
|
|
388
990
|
* RFC-005 treatment for one MCPL wire block, for the model-visible text
|
|
@@ -390,15 +992,20 @@ export function unfetchableReason(record, nowMs = Date.now()) {
|
|
|
390
992
|
* its existing (inline/text) handling. A reference's stub carries text taken
|
|
391
993
|
* from the block's own fields, so it's made well formed (a lone UTF-16
|
|
392
994
|
* surrogate becomes U+FFFD), as the context store would make it: a tool
|
|
393
|
-
* result's copies then carry the same text (#695).
|
|
995
|
+
* result's copies then carry the same text (#695). For MCP's embedded
|
|
996
|
+
* resource, it's the text for contents that can't be shown (its marker is
|
|
997
|
+
* well formed where it's made), and null where they can: the caller shows the
|
|
998
|
+
* block they make (classifyBlock's `embedded`).
|
|
394
999
|
*/
|
|
395
|
-
export function referenceStubOrNull(block, provenance, serverId) {
|
|
1000
|
+
export function referenceStubOrNull(block, provenance, serverId, options) {
|
|
396
1001
|
const c = classifyBlock(block);
|
|
397
1002
|
switch (c.kind) {
|
|
398
1003
|
case 'text': return null;
|
|
399
1004
|
case 'inline':
|
|
400
1005
|
return c.contradiction ? INLINE_WITHHELD_TEXT : null;
|
|
401
|
-
case 'reference': return buildReferenceStub(c.testimony, provenance, serverId).toWellFormed();
|
|
1006
|
+
case 'reference': return buildReferenceStub(c.testimony, provenance, serverId, options).toWellFormed();
|
|
1007
|
+
case 'embedded':
|
|
1008
|
+
return c.contents.kind === 'withheld' ? INLINE_WITHHELD_TEXT : c.contents.kind === 'marker' ? c.contents.text : null;
|
|
402
1009
|
case 'invalid': return c.shown;
|
|
403
1010
|
}
|
|
404
1011
|
}
|
|
@@ -407,26 +1014,39 @@ export function referenceStubOrNull(block, provenance, serverId) {
|
|
|
407
1014
|
* result, which carries blocks rather than text. It makes the decision
|
|
408
1015
|
* referenceStubOrNull makes for the model's lanes, since what a script prints
|
|
409
1016
|
* comes back to the model, except that a script reads every field, so inline
|
|
410
|
-
* data beside a uri of any type but null or
|
|
411
|
-
* model's lanes, which show no uri, withhold it beside a string one
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
1017
|
+
* data beside a uri of any type but null, absent or empty is withheld, where
|
|
1018
|
+
* the model's lanes, which show no uri, withhold it beside a string one that
|
|
1019
|
+
* isn't empty. A reference becomes the stub's facts as data: the host's
|
|
1020
|
+
* reference id, what the server said about it in this block (its labels
|
|
1021
|
+
* stripped and bounded as in the stub), and `savedTo`, the workspace path
|
|
1022
|
+
* its payload was fetched to, or null when it wasn't (`fetch_reference`
|
|
1023
|
+
* takes the id as `ref_id`). Its uri is left out, except that a link (MCP's
|
|
1024
|
+
* resource_link) carries `uri` where the model's line for it shows the uri
|
|
1025
|
+
* (shownLinkUri). An unsaved reference that counts as expired also carries
|
|
1026
|
+
* `expired`, and one the fetcher would refuse carries `unfetchable`, the
|
|
1027
|
+
* words a stub gives in place of advising fetch_reference
|
|
1028
|
+
* (unfetchableReason). Both give why a fetch_reference with the object's
|
|
1029
|
+
* `refId` would be refused, as the result's references claim it, so a link
|
|
1030
|
+
* whose line shows its uri, which the host can't fetch, carries them too: its
|
|
1031
|
+
* line has no id to fetch with. The fetch itself reads the record, which
|
|
1032
|
+
* keeps the latest testimony registered until it's fetched, and then the one
|
|
1033
|
+
* its saved copy was verified against (Ada-1017's call). `claims` are those
|
|
1034
|
+
* of everything the script's view shows (scriptViewClaims), so its other
|
|
1035
|
+
* references count (ResultClaims). The reference is registered bound to
|
|
1036
|
+
* `serverId`, the server whose tool returned it, as the model's lanes bind a
|
|
1037
|
+
* tool result's references, so the fetch its id offers can be made. With no
|
|
1038
|
+
* `serverId`, as for a module's result, it's registered as the model's
|
|
421
1039
|
* history registers a module's: the record its uri already has, bound where
|
|
422
1040
|
* it arrived (a module's ctx.callTool registers an MCPL peer's references),
|
|
423
1041
|
* or a new one bound to no server. A block the classifier
|
|
424
1042
|
* rejects becomes the notice the model's lanes show, and inline data that
|
|
425
|
-
* also claims a bulk disposition, or a uri of any type but null or
|
|
426
|
-
*
|
|
1043
|
+
* also claims a bulk disposition, or a uri of any type but null, absent or
|
|
1044
|
+
* empty, the withheld notice, each as a text block. MCP's embedded resource
|
|
1045
|
+
* becomes the block its contents make, as the model's lanes show it, never
|
|
1046
|
+
* its uri.
|
|
427
1047
|
* A text block, or inline data that claims nothing, is returned as sent.
|
|
428
1048
|
*/
|
|
429
|
-
export function referenceBlockForScript(block, serverId) {
|
|
1049
|
+
export function referenceBlockForScript(block, serverId, claims, line) {
|
|
430
1050
|
const c = classifyBlock(block);
|
|
431
1051
|
switch (c.kind) {
|
|
432
1052
|
case 'text': return block;
|
|
@@ -434,22 +1054,60 @@ export function referenceBlockForScript(block, serverId) {
|
|
|
434
1054
|
// A script reads every field, as history's JSON writes it, so inline
|
|
435
1055
|
// data beside a uri of any type is withheld here, not only beside a
|
|
436
1056
|
// string one as in the model's lanes, which show no uri: an object can
|
|
437
|
-
// carry a credential. An absent or
|
|
438
|
-
//
|
|
1057
|
+
// carry a credential. An absent, null or empty uri carries none, as at
|
|
1058
|
+
// any depth (hasUri), so a peer's and a module's agree (Ada-1017,
|
|
1059
|
+
// room-203 #200094); one beside a disposition is the classifier's
|
|
1060
|
+
// contradiction, a reference's claim.
|
|
439
1061
|
const uri = block.uri;
|
|
440
|
-
return c.contradiction || (uri !== undefined && uri !== null)
|
|
1062
|
+
return c.contradiction || (uri !== undefined && uri !== null && uri !== '')
|
|
441
1063
|
? { type: 'text', text: INLINE_WITHHELD_TEXT }
|
|
442
1064
|
: block;
|
|
443
1065
|
}
|
|
444
1066
|
case 'invalid': return { type: 'text', text: c.shown };
|
|
1067
|
+
case 'embedded':
|
|
1068
|
+
// The block its contents make, as the model's lanes show it, and never
|
|
1069
|
+
// its uri or its `resource` record (Ada-1017, room-203 #206688): text,
|
|
1070
|
+
// or inline data that claims nothing, as such a block reaches a script;
|
|
1071
|
+
// the withheld notice where it claims a disposition; its marker
|
|
1072
|
+
// otherwise. Given again, each is as it was.
|
|
1073
|
+
if (c.contents.kind === 'block')
|
|
1074
|
+
return c.contents.block;
|
|
1075
|
+
return { type: 'text', text: c.contents.kind === 'withheld' ? INLINE_WITHHELD_TEXT : c.contents.text };
|
|
445
1076
|
case 'reference': {
|
|
446
1077
|
const t = c.testimony;
|
|
447
1078
|
const record = referenceRegistry.register(t, serverId);
|
|
448
|
-
//
|
|
449
|
-
|
|
1079
|
+
// One clock reading for the whole object, so its words can't disagree
|
|
1080
|
+
// with each other across an expiry.
|
|
1081
|
+
const nowMs = Date.now();
|
|
1082
|
+
// A link's uri where the model's line for the result shows it, by the
|
|
1083
|
+
// claims the line reads and its record as the line reads it (`line`,
|
|
1084
|
+
// where they differ from the view's), and never where the view's
|
|
1085
|
+
// claims withhold it: reading more claims can loosen as well as
|
|
1086
|
+
// tighten, since an expiry makes a link one the host can't fetch, whose
|
|
1087
|
+
// uri the line shows (Ada-1017, room-203 #201013). The line's record is
|
|
1088
|
+
// the registry's as the model's line leaves it, before the script
|
|
1089
|
+
// lane's own registrations (#202683, #203011), since the treatment
|
|
1090
|
+
// registers, rebinds and evicts as it goes, which the line never sees;
|
|
1091
|
+
// where there was none, it's the one the link's own registration
|
|
1092
|
+
// makes. A script never sees a uri the model's line
|
|
1093
|
+
// withholds, through its blocks; structured content goes as sent, as
|
|
1094
|
+
// on main.
|
|
1095
|
+
const lineRecord = line
|
|
1096
|
+
? line.recordBefore(t.uri) ?? { refId: record.refId, serverIds: serverId === undefined ? [] : [serverId], testimony: t, receivedAt: record.receivedAt }
|
|
1097
|
+
: record;
|
|
1098
|
+
const lineShows = shownLinkUri(t, lineRecord, nowMs, line ? line.claims : claims);
|
|
1099
|
+
const uri = lineShows !== null && line !== undefined && claimedNever(record, claims) ? null : lineShows;
|
|
1100
|
+
// Why a fetch_reference with this object's refId would be refused, in
|
|
1101
|
+
// the words a stub gives, whether or not the model's line for it is a
|
|
1102
|
+
// stub: the line has no id to fetch with, and the object does. As the
|
|
1103
|
+
// stub judges it, a saved copy is still served, and the result's own
|
|
1104
|
+
// claims decide where its references name the uri (uriClaims).
|
|
1105
|
+
const expiry = record.fetchedPath ? null : claimedExpiry(record, claims, nowMs);
|
|
1106
|
+
const unfetchable = unfetchableReason(record, nowMs, claims);
|
|
450
1107
|
return {
|
|
451
1108
|
type: block.type,
|
|
452
1109
|
refId: record.refId,
|
|
1110
|
+
...(uri !== null ? { uri } : {}),
|
|
453
1111
|
...(t.name !== undefined ? { name: sanitizeLabel(t.name, STUB_FIELD_CHARS) } : {}),
|
|
454
1112
|
...(t.mimeType !== undefined ? { mimeType: sanitizeLabel(t.mimeType, STUB_FIELD_CHARS) } : {}),
|
|
455
1113
|
...(t.sizeBytes !== undefined ? { sizeBytes: t.sizeBytes } : {}),
|
|
@@ -457,6 +1115,7 @@ export function referenceBlockForScript(block, serverId) {
|
|
|
457
1115
|
...(t.expiresAt !== undefined ? { expiresAt: sanitizeLabel(t.expiresAt, STUB_FIELD_CHARS) } : {}),
|
|
458
1116
|
savedTo: record.fetchedPath ?? null,
|
|
459
1117
|
...(expiry ? { expired: EXPIRY_REASON[expiry] } : {}),
|
|
1118
|
+
...(unfetchable !== null ? { unfetchable } : {}),
|
|
460
1119
|
};
|
|
461
1120
|
}
|
|
462
1121
|
}
|