blob: 76b17b7460470c4f046d6a7bfadde8ff04bd61e0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildWebFetchTool } from '@maka/runtime/web-fetch-tool';
import { assertAllowedTarget, createLocalWebFetchExecutor } from '@maka/runtime/local-web-fetch';
import {
createProxiedFetchTransport,
type ProxiedFetchProxy,
type ProxiedFetchTransport,
} from '@maka/runtime/network/scoped-fetch-transport';
import { type MakaTool } from '@maka/runtime/tool-runtime';
import type { RuntimePolicyOperationCoordinator } from '@maka/storage/runtime-policy-stores';
import { toRuntimePolicyProxy } from './runtime-policy-proxy.js';
interface HostWebFetchServiceInput {
readonly probeTimeoutMs?: number;
readonly policy: Pick<RuntimePolicyOperationCoordinator, 'resolveHostOutboundExecution'>;
readonly createFetchTransport?: (proxy: ProxiedFetchProxy | null) => ProxiedFetchTransport;
}
export interface HostWebFetchService {
fetch(input: {
readonly url: string;
readonly sessionId: string;
readonly abortSignal?: AbortSignal;
}): Promise<string>;
probe(input: {
url: string;
sessionId: string;
abortSignal: AbortSignal;
}): Promise<{ status: number; statusText?: string; elapsedMs: number }>;
}
export function createHostWebFetchService(input: HostWebFetchServiceInput): HostWebFetchService {
const createFetchTransport = input.createFetchTransport ?? createProxiedFetchTransport;
return {
fetch: async ({ url, sessionId, abortSignal }) => {
const resolved = await input.policy.resolveHostOutboundExecution();
if (resolved.kind === 'privacy_mode') {
throw new Error('WebFetch is disabled while privacy mode is active.');
}
if (resolved.kind === 'credential_not_configured') {
throw new Error('Configure the network proxy credential before using WebFetch.');
}
const transport = createFetchTransport(
toRuntimePolicyProxy(resolved.networkProxy, resolved.secretMaterial.networkProxy?.secret),
);
try {
return await createLocalWebFetchExecutor({ fetch: transport.fetch }).fetch({
url,
sessionId,
...(abortSignal ? { abortSignal } : {}),
});
} finally {
await transport.close();
}
},
probe: async ({ url, abortSignal }) => {
const parsed = new URL(url);
assertAllowedTarget(parsed);
abortSignal.throwIfAborted();
const resolved = await input.policy.resolveHostOutboundExecution();
if (resolved.kind === 'privacy_mode')
throw new Error('Endpoint health checks are disabled while privacy mode is active.');
if (resolved.kind === 'credential_not_configured')
throw new Error('Configure the network proxy credential before checking an endpoint.');
const transport = createFetchTransport(
toRuntimePolicyProxy(resolved.networkProxy, resolved.secretMaterial.networkProxy?.secret),
);
const started = Date.now();
const timeout = new AbortController();
const timer = setTimeout(
() => timeout.abort(new Error('Endpoint health probe timed out.')),
input.probeTimeoutMs ?? 30_000,
);
const signal = AbortSignal.any([abortSignal, timeout.signal]);
try {
let response = await transport.fetch(parsed, {
method: 'HEAD',
redirect: 'manual',
signal,
});
await response.body?.cancel();
if (response.status === 405 || response.status === 501) {
response = await transport.fetch(parsed, { method: 'GET', redirect: 'manual', signal });
await response.body?.cancel();
}
return {
status: response.status,
...(response.statusText ? { statusText: response.statusText } : {}),
elapsedMs: Date.now() - started,
};
} finally {
clearTimeout(timer);
await transport.close();
}
},
};
}
export function createHostWebFetchTool(input: HostWebFetchServiceInput): MakaTool {
return createHostWebFetchToolFromService(createHostWebFetchService(input));
}
export function createHostWebFetchToolFromService(service: HostWebFetchService): MakaTool {
return buildWebFetchTool({
fetch: ({ url, sessionId, abortSignal }) =>
service.fetch({ url, sessionId, ...(abortSignal ? { abortSignal } : {}) }),
});
}