@namzu/sdk 40.0.0 → 42.0.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/CHANGELOG.md +236 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +3 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +11 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/runAgent.d.ts +14 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +3 -0
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +8 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +11 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +92 -4
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/audio-admission.d.ts +17 -0
- package/dist/connector/mcp/audio-admission.d.ts.map +1 -0
- package/dist/connector/mcp/audio-admission.js +171 -0
- package/dist/connector/mcp/audio-admission.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +252 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +611 -39
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/envelope.d.ts +91 -0
- package/dist/connector/mcp/envelope.d.ts.map +1 -0
- package/dist/connector/mcp/envelope.js +173 -0
- package/dist/connector/mcp/envelope.js.map +1 -0
- package/dist/connector/mcp/era.d.ts +130 -0
- package/dist/connector/mcp/era.d.ts.map +1 -0
- package/dist/connector/mcp/era.js +304 -0
- package/dist/connector/mcp/era.js.map +1 -0
- package/dist/connector/mcp/errors.d.ts +106 -0
- package/dist/connector/mcp/errors.d.ts.map +1 -0
- package/dist/connector/mcp/errors.js +154 -0
- package/dist/connector/mcp/errors.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts +11 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +21 -6
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +7 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +10 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +83 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +177 -11
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/connector/mcp/x-mcp-header.d.ts +56 -0
- package/dist/connector/mcp/x-mcp-header.d.ts.map +1 -0
- package/dist/connector/mcp/x-mcp-header.js +254 -0
- package/dist/connector/mcp/x-mcp-header.js.map +1 -0
- package/dist/constants/mcp/index.d.ts +123 -15
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +135 -16
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +43 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +20 -0
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +19 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/prompt/index.d.ts +1 -1
- package/dist/prompt/index.d.ts.map +1 -1
- package/dist/prompt/index.js +1 -1
- package/dist/prompt/index.js.map +1 -1
- package/dist/public-runtime.d.ts +7 -4
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +16 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -1
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +4 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +10 -1
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +11 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +2 -0
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +6 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +6 -0
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/guardrail-presets.d.ts +187 -1
- package/dist/runtime/query/guardrail-presets.d.ts.map +1 -1
- package/dist/runtime/query/guardrail-presets.js +298 -0
- package/dist/runtime/query/guardrail-presets.js.map +1 -1
- package/dist/runtime/query/index.d.ts +14 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +3 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +3 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +46 -3
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +8 -0
- package/dist/scheduler/local.js.map +1 -1
- package/dist/store/run/disk.d.ts +35 -1
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +100 -0
- package/dist/store/run/disk.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +17 -2
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +17 -3
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/untrusted-envelope.d.ts +35 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +91 -3
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/agent/base.d.ts +23 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +20 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +39 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +205 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +56 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/store.d.ts +41 -0
- package/dist/types/run/store.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +68 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +19 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +3 -0
- package/src/agents/SupervisorAgent.ts +11 -0
- package/src/agents/runAgent.ts +18 -0
- package/src/bridge/a2a/mapper.ts +8 -0
- package/src/bridge/sse/mapper.ts +11 -0
- package/src/connector/index.ts +27 -0
- package/src/connector/mcp/adapter.ts +103 -4
- package/src/connector/mcp/audio-admission.ts +173 -0
- package/src/connector/mcp/client.ts +694 -45
- package/src/connector/mcp/envelope.ts +235 -0
- package/src/connector/mcp/era.ts +400 -0
- package/src/connector/mcp/errors.ts +171 -0
- package/src/connector/mcp/http-sse.ts +23 -6
- package/src/connector/mcp/index.ts +37 -0
- package/src/connector/mcp/streamable-http.ts +199 -11
- package/src/connector/mcp/x-mcp-header.ts +322 -0
- package/src/constants/mcp/index.ts +145 -16
- package/src/manager/agent/lifecycle.ts +51 -0
- package/src/prompt/coding-agent-doctrine.ts +31 -4
- package/src/prompt/index.ts +1 -0
- package/src/public-runtime.ts +42 -0
- package/src/public-tools.ts +8 -2
- package/src/registry/tool/execute.ts +9 -1
- package/src/runtime/bidi/session.ts +13 -0
- package/src/runtime/query/executor.ts +13 -0
- package/src/runtime/query/guardrail-presets.ts +356 -0
- package/src/runtime/query/index.ts +17 -0
- package/src/runtime/query/tooling.ts +5 -0
- package/src/sandbox/provider/local.ts +45 -2
- package/src/scheduler/local.ts +8 -0
- package/src/store/run/disk.ts +108 -0
- package/src/tools/coordinator/agent.ts +17 -2
- package/src/tools/coordinator/index.ts +17 -3
- package/src/tools/untrusted-envelope.ts +94 -3
- package/src/types/agent/base.ts +24 -0
- package/src/types/agent/scheduler.ts +21 -0
- package/src/types/agent/task.ts +41 -0
- package/src/types/connector/mcp.ts +205 -1
- package/src/types/run/events.ts +56 -0
- package/src/types/run/store.ts +42 -0
- package/src/types/sandbox/index.ts +69 -1
- package/src/types/tool/index.ts +20 -0
|
@@ -9,7 +9,9 @@ import type {
|
|
|
9
9
|
} from '../../types/connector/index.js'
|
|
10
10
|
import type { ToolResultBlock, ToolResultContent } from '../../types/message/index.js'
|
|
11
11
|
import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js'
|
|
12
|
+
import { admitMcpAudioBatch } from './audio-admission.js'
|
|
12
13
|
import type { MCPClient } from './client.js'
|
|
14
|
+
import { MCPInputRequiredError, isMissingRequiredClientCapabilityError } from './errors.js'
|
|
13
15
|
import { MCPHttpRedirectError } from './http-redirect.js'
|
|
14
16
|
import { admitMcpImageBatch } from './image-admission.js'
|
|
15
17
|
|
|
@@ -516,12 +518,68 @@ export function mcpToolToToolDefinition(
|
|
|
516
518
|
},
|
|
517
519
|
}
|
|
518
520
|
}
|
|
521
|
+
// MRTR: a conforming server never sends this, since namzu declares
|
|
522
|
+
// `clientCapabilities: {}` — but a non-conforming one, or a second
|
|
523
|
+
// `input_required` after the one automatic retry `MCPClient`
|
|
524
|
+
// already made, is named here rather than reaching the caller as
|
|
525
|
+
// an unexplained rejection. No side effect is known to have
|
|
526
|
+
// happened on this path (the spec's model is that a call ending
|
|
527
|
+
// in `input_required` has not truly run yet), so a retry — of
|
|
528
|
+
// this tool, or a different approach entirely — is safe.
|
|
529
|
+
if (error instanceof MCPInputRequiredError) {
|
|
530
|
+
const requested = error.inputRequests.map((request) => request.method)
|
|
531
|
+
return frameServerResult(
|
|
532
|
+
{
|
|
533
|
+
success: false,
|
|
534
|
+
output: '',
|
|
535
|
+
error: `MCP tool "${tool.name}" on server "${serverName}" asked for input this client has no way to supply${requested.length > 0 ? ` (${requested.join(', ')})` : ''}.`,
|
|
536
|
+
data: {
|
|
537
|
+
code: 'mcp_tool_input_required',
|
|
538
|
+
server: serverName,
|
|
539
|
+
tool: tool.name,
|
|
540
|
+
requested,
|
|
541
|
+
retrySafety: 'safe',
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
serverName,
|
|
545
|
+
tool.name,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
// Same principle, for the other outcome a no-capability host is
|
|
549
|
+
// genuinely likely to see: the server refused before running the
|
|
550
|
+
// tool because this client never declared a capability it needs.
|
|
551
|
+
if (isMissingRequiredClientCapabilityError(error)) {
|
|
552
|
+
const requiredCapabilities = requiredCapabilitiesFrom(error.data)
|
|
553
|
+
return frameServerResult(
|
|
554
|
+
{
|
|
555
|
+
success: false,
|
|
556
|
+
output: '',
|
|
557
|
+
error: `MCP tool "${tool.name}" on server "${serverName}" requires client capabilities this client did not declare${requiredCapabilities.length > 0 ? ` (${requiredCapabilities.join(', ')})` : ''}.`,
|
|
558
|
+
data: {
|
|
559
|
+
code: 'mcp_tool_missing_client_capability',
|
|
560
|
+
server: serverName,
|
|
561
|
+
tool: tool.name,
|
|
562
|
+
requiredCapabilities,
|
|
563
|
+
retrySafety: 'safe',
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
serverName,
|
|
567
|
+
tool.name,
|
|
568
|
+
)
|
|
569
|
+
}
|
|
519
570
|
throw error
|
|
520
571
|
}
|
|
521
572
|
},
|
|
522
573
|
}
|
|
523
574
|
}
|
|
524
575
|
|
|
576
|
+
/** Read `-32021`'s `data.requiredCapabilities` defensively — it is a server's own claim, not a validated shape. */
|
|
577
|
+
function requiredCapabilitiesFrom(data: unknown): string[] {
|
|
578
|
+
if (typeof data !== 'object' || data === null) return []
|
|
579
|
+
const required = (data as { requiredCapabilities?: unknown }).requiredCapabilities
|
|
580
|
+
return Array.isArray(required) ? required.filter((c): c is string => typeof c === 'string') : []
|
|
581
|
+
}
|
|
582
|
+
|
|
525
583
|
export function toolDefinitionToMCPTool(tool: ToolDefinition): MCPToolDefinition {
|
|
526
584
|
return {
|
|
527
585
|
name: tool.name,
|
|
@@ -609,6 +667,11 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
609
667
|
block.type === 'image',
|
|
610
668
|
)
|
|
611
669
|
const imagesAdmitted = admitMcpImageBatch(imageBlocks)
|
|
670
|
+
const audioBlocks = result.content.filter(
|
|
671
|
+
(block): block is Extract<(typeof result.content)[number], { type: 'audio' }> =>
|
|
672
|
+
block.type === 'audio',
|
|
673
|
+
)
|
|
674
|
+
const audiosAdmitted = admitMcpAudioBatch(audioBlocks)
|
|
612
675
|
|
|
613
676
|
// Non-text blocks used to be filtered out and never seen again: a
|
|
614
677
|
// bridged MCP server returning a chart, a screenshot or a PDF had that
|
|
@@ -617,6 +680,13 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
617
680
|
// as model-visible content when any are present.
|
|
618
681
|
const blocks: ToolResultBlock[] = []
|
|
619
682
|
const pins: WorkingStatePin[] = []
|
|
683
|
+
// Set only by a block type that has no `ToolResultBlock` shape of its
|
|
684
|
+
// own and is rendered as a plain text pointer instead (`resource_link`
|
|
685
|
+
// today). Without this, a result made up entirely of such pointers
|
|
686
|
+
// would compute `hasRichContent` false — every pushed block LOOKS like
|
|
687
|
+
// ordinary text — and the whole `content` array, the pointer's only
|
|
688
|
+
// carrier, would be dropped by the `hasRichContent` gate below.
|
|
689
|
+
let hasNamedPointer = false
|
|
620
690
|
for (const block of result.content) {
|
|
621
691
|
if (block.type === 'resource' && block.resource?.mimeType === WORKING_STATE_MIME) {
|
|
622
692
|
// A server pins facts by returning them as a resource of this type:
|
|
@@ -640,14 +710,31 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
640
710
|
}
|
|
641
711
|
: {}),
|
|
642
712
|
})
|
|
713
|
+
} else if (block.type === 'resource_link') {
|
|
714
|
+
// A resource_link never carries content, admitted or not — it is
|
|
715
|
+
// a pointer the model cannot dereference, so it is named rather
|
|
716
|
+
// than pretended to be present, exactly like a URI-only embedded
|
|
717
|
+
// resource just below.
|
|
718
|
+
blocks.push({
|
|
719
|
+
type: 'text',
|
|
720
|
+
text: `[MCP resource link: ${block.name} (${block.uri})]`,
|
|
721
|
+
})
|
|
722
|
+
hasNamedPointer = true
|
|
643
723
|
} else if (block.type === 'resource' && block.resource?.text) {
|
|
644
724
|
// A resource with inline text is readable content; one that is
|
|
645
|
-
// only a URI is a pointer the model cannot
|
|
646
|
-
// named rather than pretended to be
|
|
725
|
+
// only a URI, or only a `blob`, is a pointer the model cannot
|
|
726
|
+
// dereference, so it is named rather than pretended to be
|
|
727
|
+
// present — here, that means it is left out of `blocks`
|
|
728
|
+
// entirely rather than rendered as fabricated text.
|
|
647
729
|
blocks.push({ type: 'text', text: block.resource.text })
|
|
648
730
|
}
|
|
731
|
+
// `audio` has no `ToolResultBlock` shape of its own today, so it is
|
|
732
|
+
// summarized into `output` below rather than pushed here — the
|
|
733
|
+
// same reasoning that keeps a withheld image out of plain text,
|
|
734
|
+
// applied to a media type this adapter cannot pass through as
|
|
735
|
+
// bytes at all yet.
|
|
649
736
|
}
|
|
650
|
-
const hasRichContent = blocks.some((b) => b.type !== 'text')
|
|
737
|
+
const hasRichContent = hasNamedPointer || blocks.some((b) => b.type !== 'text')
|
|
651
738
|
|
|
652
739
|
// A server may answer with a structured payload and skip the
|
|
653
740
|
// compatibility text block. Serializing it is the difference between
|
|
@@ -662,7 +749,19 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
662
749
|
imageBlocks.length > 0 && !imagesAdmitted
|
|
663
750
|
? '[MCP image batch withheld from model input: one or more blocks are not complete supported raster containers matching their declared media types.]'
|
|
664
751
|
: ''
|
|
665
|
-
|
|
752
|
+
// Audio has no `ToolResultBlock` carrier, so an admitted clip is named
|
|
753
|
+
// in `output` by its media type instead of being dumped into `content`
|
|
754
|
+
// as base64 text — the same mistake this adapter already fixed once for
|
|
755
|
+
// screenshots. A withheld batch gets the same notice shape as images.
|
|
756
|
+
const audioNotice =
|
|
757
|
+
audioBlocks.length === 0
|
|
758
|
+
? ''
|
|
759
|
+
: audiosAdmitted
|
|
760
|
+
? `[MCP audio: ${audioBlocks.map((block) => block.mimeType).join(', ')}]`
|
|
761
|
+
: '[MCP audio batch withheld from model input: one or more blocks are not complete supported audio containers matching their declared media types.]'
|
|
762
|
+
const output = [visibleText, invalidImageNotice, audioNotice]
|
|
763
|
+
.filter((part) => part.length > 0)
|
|
764
|
+
.join('\n')
|
|
666
765
|
|
|
667
766
|
return {
|
|
668
767
|
...(pins.length > 0 ? { workingState: pins } : {}),
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { MCPContentBlock } from '../../types/connector/index.js'
|
|
2
|
+
|
|
3
|
+
const MAX_ENCODED_BYTES = 24 * 1024 * 1024
|
|
4
|
+
|
|
5
|
+
type MCPAudioBlock = Extract<MCPContentBlock, { type: 'audio' }>
|
|
6
|
+
type AudioMediaType = 'audio/wav' | 'audio/mpeg' | 'audio/ogg'
|
|
7
|
+
|
|
8
|
+
const AUDIO_MEDIA_TYPES = new Set<AudioMediaType>(['audio/wav', 'audio/mpeg', 'audio/ogg'])
|
|
9
|
+
|
|
10
|
+
function isAudioMediaType(value: string): value is AudioMediaType {
|
|
11
|
+
return AUDIO_MEDIA_TYPES.has(value as AudioMediaType)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function decodeCanonicalBase64(value: string): Uint8Array | null {
|
|
15
|
+
if (value.length === 0 || value.length > MAX_ENCODED_BYTES) return null
|
|
16
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
const decoded = Buffer.from(value, 'base64')
|
|
20
|
+
return decoded.toString('base64') === value ? decoded : null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A canonical RIFF/WAVE container: the size field agrees with the buffer
|
|
25
|
+
* length, and walking its chunks (word-aligned, as RIFF requires) lands
|
|
26
|
+
* exactly on the end of the buffer having seen both a `fmt ` and a `data`
|
|
27
|
+
* chunk. Mirrors the WebP walker in `image-admission.ts`, which is the same
|
|
28
|
+
* RIFF container format wearing a different fourCC.
|
|
29
|
+
*/
|
|
30
|
+
function isWav(bytes: Uint8Array): boolean {
|
|
31
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
32
|
+
if (
|
|
33
|
+
bytes.length < 12 ||
|
|
34
|
+
buffer.toString('ascii', 0, 4) !== 'RIFF' ||
|
|
35
|
+
buffer.readUInt32LE(4) + 8 !== bytes.length ||
|
|
36
|
+
buffer.toString('ascii', 8, 12) !== 'WAVE'
|
|
37
|
+
) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
let offset = 12
|
|
41
|
+
let sawFmt = false
|
|
42
|
+
let sawData = false
|
|
43
|
+
while (offset + 8 <= bytes.length) {
|
|
44
|
+
const chunkId = buffer.toString('ascii', offset, offset + 4)
|
|
45
|
+
const chunkSize = buffer.readUInt32LE(offset + 4)
|
|
46
|
+
const dataStart = offset + 8
|
|
47
|
+
const dataEnd = dataStart + chunkSize
|
|
48
|
+
if (dataEnd < dataStart || dataEnd > bytes.length) return false
|
|
49
|
+
if (chunkId === 'fmt ') {
|
|
50
|
+
if (chunkSize < 16) return false
|
|
51
|
+
sawFmt = true
|
|
52
|
+
}
|
|
53
|
+
if (chunkId === 'data') sawData = true
|
|
54
|
+
// RIFF chunks are padded to an even byte count; the pad byte is not
|
|
55
|
+
// counted in chunkSize but is present in the stream.
|
|
56
|
+
offset = dataEnd + (chunkSize % 2)
|
|
57
|
+
}
|
|
58
|
+
return sawFmt && sawData && offset === bytes.length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A sequence of Ogg pages, each framed by its `OggS` capture pattern and a
|
|
63
|
+
* segment table whose declared sizes account for every byte of payload.
|
|
64
|
+
* Page checksums are not verified — the framing itself, consumed exactly to
|
|
65
|
+
* the end of the buffer, is what this admits or refuses.
|
|
66
|
+
*/
|
|
67
|
+
function isOgg(bytes: Uint8Array): boolean {
|
|
68
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
69
|
+
let offset = 0
|
|
70
|
+
let sawPage = false
|
|
71
|
+
while (offset + 27 <= bytes.length) {
|
|
72
|
+
if (buffer.toString('ascii', offset, offset + 4) !== 'OggS') return false
|
|
73
|
+
if (bytes[offset + 4] !== 0) return false // stream_structure_version
|
|
74
|
+
const segmentCount = bytes[offset + 26] ?? 0
|
|
75
|
+
const segmentTableStart = offset + 27
|
|
76
|
+
if (segmentTableStart + segmentCount > bytes.length) return false
|
|
77
|
+
let payloadLength = 0
|
|
78
|
+
for (let i = 0; i < segmentCount; i += 1) {
|
|
79
|
+
payloadLength += bytes[segmentTableStart + i] ?? 0
|
|
80
|
+
}
|
|
81
|
+
const payloadEnd = segmentTableStart + segmentCount + payloadLength
|
|
82
|
+
if (payloadEnd > bytes.length) return false
|
|
83
|
+
sawPage = true
|
|
84
|
+
offset = payloadEnd
|
|
85
|
+
}
|
|
86
|
+
return sawPage && offset === bytes.length
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MPEG-1 Layer III bitrates in kbps, indexed by the header's 4-bit field.
|
|
90
|
+
// Index 0 (free bitrate) and 15 (reserved) are both treated as unsupported.
|
|
91
|
+
const MPEG1_L3_BITRATES: readonly number[] = [
|
|
92
|
+
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
|
|
93
|
+
]
|
|
94
|
+
const MPEG1_SAMPLE_RATES: readonly number[] = [44100, 48000, 32000, 0]
|
|
95
|
+
|
|
96
|
+
/** Frame length in bytes for an MPEG-1 Layer III frame, or null if the header is malformed/unsupported. */
|
|
97
|
+
function mp3FrameLength(headerByte1: number, headerByte2: number): number | null {
|
|
98
|
+
const versionBits = (headerByte1 >> 3) & 0x3
|
|
99
|
+
const layerBits = (headerByte1 >> 1) & 0x3
|
|
100
|
+
if (versionBits !== 0x3 || layerBits !== 0x1) return null // MPEG-1, Layer III only
|
|
101
|
+
const bitrateIndex = (headerByte2 >> 4) & 0xf
|
|
102
|
+
const sampleRateIndex = (headerByte2 >> 2) & 0x3
|
|
103
|
+
const padding = (headerByte2 >> 1) & 0x1
|
|
104
|
+
if (bitrateIndex === 0 || bitrateIndex === 0xf || sampleRateIndex === 0x3) return null
|
|
105
|
+
const bitrate = MPEG1_L3_BITRATES[bitrateIndex]
|
|
106
|
+
const sampleRate = MPEG1_SAMPLE_RATES[sampleRateIndex]
|
|
107
|
+
if (!bitrate || !sampleRate) return null
|
|
108
|
+
return Math.floor((144 * bitrate * 1000) / sampleRate) + padding
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function id3v2TagLength(bytes: Uint8Array, buffer: Buffer): number {
|
|
112
|
+
if (bytes.length < 10 || buffer.toString('ascii', 0, 3) !== 'ID3') return 0
|
|
113
|
+
// Synchsafe integer: 7 significant bits per byte, big-endian.
|
|
114
|
+
const size =
|
|
115
|
+
((bytes[6] ?? 0) & 0x7f) * 0x200000 +
|
|
116
|
+
((bytes[7] ?? 0) & 0x7f) * 0x4000 +
|
|
117
|
+
((bytes[8] ?? 0) & 0x7f) * 0x80 +
|
|
118
|
+
((bytes[9] ?? 0) & 0x7f)
|
|
119
|
+
return 10 + size
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* MPEG-1 Layer III frames, one after another with no trailing bytes: an
|
|
124
|
+
* optional ID3v2 tag is skipped, then every remaining byte must belong to a
|
|
125
|
+
* syntactically valid frame header whose declared length reaches exactly
|
|
126
|
+
* the next frame (or the end of the buffer). Other MPEG versions and layers
|
|
127
|
+
* are refused rather than approximated.
|
|
128
|
+
*/
|
|
129
|
+
function isMp3(bytes: Uint8Array): boolean {
|
|
130
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
131
|
+
const offsetAfterTag = id3v2TagLength(bytes, buffer)
|
|
132
|
+
if (offsetAfterTag + 4 > bytes.length) return false
|
|
133
|
+
let offset = offsetAfterTag
|
|
134
|
+
let sawFrame = false
|
|
135
|
+
while (offset + 4 <= bytes.length) {
|
|
136
|
+
if (bytes[offset] !== 0xff || ((bytes[offset + 1] ?? 0) & 0xe0) !== 0xe0) return false
|
|
137
|
+
const frameLength = mp3FrameLength(bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0)
|
|
138
|
+
if (frameLength === null || frameLength < 4 || offset + frameLength > bytes.length) {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
sawFrame = true
|
|
142
|
+
offset += frameLength
|
|
143
|
+
}
|
|
144
|
+
return sawFrame && offset === bytes.length
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function audioIsAdmitted(block: MCPAudioBlock): boolean {
|
|
148
|
+
if (!isAudioMediaType(block.mimeType)) return false
|
|
149
|
+
const bytes = decodeCanonicalBase64(block.data)
|
|
150
|
+
if (!bytes) return false
|
|
151
|
+
switch (block.mimeType) {
|
|
152
|
+
case 'audio/wav':
|
|
153
|
+
return isWav(bytes)
|
|
154
|
+
case 'audio/ogg':
|
|
155
|
+
return isOgg(bytes)
|
|
156
|
+
case 'audio/mpeg':
|
|
157
|
+
return isMp3(bytes)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Admit an MCP audio batch atomically before any member becomes model input.
|
|
163
|
+
*
|
|
164
|
+
* Mirrors `admitMcpImageBatch`: a remote server controls the declared MIME
|
|
165
|
+
* and bytes, canonical base64 is only an encoding property, and the decoded
|
|
166
|
+
* value must also be a complete, bounded container whose framing agrees
|
|
167
|
+
* with the declared media type. One bad member withholds the complete batch
|
|
168
|
+
* so a model is never shown a partial result while the host retains every
|
|
169
|
+
* raw block for inspection.
|
|
170
|
+
*/
|
|
171
|
+
export function admitMcpAudioBatch(audios: readonly MCPAudioBlock[]): boolean {
|
|
172
|
+
return audios.length === 0 || audios.every(audioIsAdmitted)
|
|
173
|
+
}
|