Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use `awf --reflect`. It prints the `/reflect` JSON response to stdout.
- **Declarative config support**: `--config <path>` with JSON/YAML + published JSON Schema
- **Domain and URL controls**: allow/deny domain rules, SSL Bump (`--ssl-bump`), and URL patterns (`--allow-urls`, requires `--ssl-bump`)
- **Data protection controls**: DLP scanning (`--enable-dlp`), DNS-over-HTTPS, and agent runtime limits (`--agent-timeout`)
- **API proxy capabilities**: OpenAI, Anthropic, Copilot, Gemini, and Google Vertex AI targets with rate limits, token steering, and Anthropic auto-cache
- **API proxy capabilities**: OpenAI, Anthropic, Copilot, Gemini, and Google Vertex AI targets with rate limits, token steering, Anthropic auto-cache, and `apiProxy.hostedWeb.{claude,codex}` domain policies
- **Infrastructure flexibility**: upstream proxy chaining, host service access, Docker-in-Docker, custom mounts, memory limits, and TTY mode
- **Operational tooling**: pre-download images and inspect logs/stats/summaries/audits from live or saved runs

Expand Down
2 changes: 1 addition & 1 deletion containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ RUN npm ci --omit=dev
COPY server.js logging.js metrics.js rate-limiter.js rate-limiter-window.js \
token-tracker.js token-persistence.js token-parsers.js \
token-tracker-http.js token-tracker-ws.js token-tracker-shared.js \
model-resolver.js model-fallback.js model-utils.js model-body-rewriter.js proxy-utils.js oidc-adapter-utils.js adapter-factory.js anthropic-transforms.js claude-hosted-web.js \
model-resolver.js model-fallback.js model-utils.js model-body-rewriter.js proxy-utils.js oidc-adapter-utils.js adapter-factory.js anthropic-transforms.js claude-hosted-web.js codex-hosted-web.js hosted-web-policy.js \
model-config.js key-validation.js server-factory.js startup.js \
proxy-request.js request-headers.js upstream-http.js proxy-guards.js proxy-error-handler.js http-client.js body-handler.js model-discovery.js management.js oidc-token-provider.js \
oidc-token-provider-base.js \
Expand Down
75 changes: 3 additions & 72 deletions containers/api-proxy/claude-hosted-web.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { isValidDomain, parseHostedWebPolicy } = require('./hosted-web-policy');

/**
* Claude hosted-web policy enforcement for the AWF API proxy.
*
Expand Down Expand Up @@ -43,12 +45,6 @@ const HOSTED_WEB_TOOL_PATTERN = /^web_(search|fetch)_(\d{4})(\d{2})(\d{2})$/;
*/
const HOSTED_WEB_TOOL_PREFIX = /^web_(search|fetch)(_|$)/;

/** Lowercase DNS label characters (validated label-by-label; see isValidDomain). */
const LABEL_CHARS = /^[a-z0-9-]+$/;
const DIGITS_ONLY = /^\d+$/;
const MAX_DOMAIN_LENGTH = 253;
const MAX_LABEL_LENGTH = 63;

/**
* Error thrown when a request violates the configured hosted-web policy.
* `statusCode` and `code` are surfaced verbatim by proxy-request.js.
Expand Down Expand Up @@ -88,29 +84,6 @@ function isHostedWebToolCandidate(type) {
return typeof type === 'string' && HOSTED_WEB_TOOL_PREFIX.test(type);
}

/**
* Same syntax as the AWF config schema: a lowercase DNS hostname with at least
* two labels, no scheme/port/path/wildcard and no raw IPv4 address. Checked
* label-by-label rather than with one nested-quantifier regex so untrusted,
* request-supplied values cannot trigger catastrophic backtracking.
*
* @param {unknown} value
* @returns {boolean}
*/
function isValidDomain(value) {
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_DOMAIN_LENGTH) return false;
const labels = value.split('.');
if (labels.length < 2) return false;
if (labels.every(label => DIGITS_ONLY.test(label))) return false; // raw IPv4
return labels.every(label => (
label.length > 0 &&
label.length <= MAX_LABEL_LENGTH &&
LABEL_CHARS.test(label) &&
!label.startsWith('-') &&
!label.endsWith('-')
));
}

/**
* Parse and validate the serialized policy from the sidecar environment.
* Throws on any malformed policy so the sidecar fails at startup rather than
Expand All @@ -120,49 +93,7 @@ function isValidDomain(value) {
* @returns {{ enabled: boolean, mode: 'allow'|'block'|null, domains: string[], maxUses?: number }|null}
*/
function parseClaudeHostedWebPolicy(raw) {
if (raw === undefined || raw === null || String(raw).trim() === '') return null;

let parsed;
try {
parsed = JSON.parse(String(raw));
} catch (err) {
throw new Error(`AWF_CLAUDE_HOSTED_WEB_POLICY is not valid JSON: ${err.message}`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('AWF_CLAUDE_HOSTED_WEB_POLICY must be a JSON object');
}
if (typeof parsed.enabled !== 'boolean') {
throw new Error('AWF_CLAUDE_HOSTED_WEB_POLICY.enabled must be a boolean');
}

if (!parsed.enabled) {
return { enabled: false, mode: null, domains: [] };
}

if (parsed.mode !== 'allow' && parsed.mode !== 'block') {
throw new Error('AWF_CLAUDE_HOSTED_WEB_POLICY.mode must be "allow" or "block" when enabled');
}
if (!Array.isArray(parsed.domains) || parsed.domains.length === 0) {
throw new Error('AWF_CLAUDE_HOSTED_WEB_POLICY.domains must be a non-empty array when enabled');
}
const domains = [];
for (const domain of parsed.domains) {
if (!isValidDomain(domain)) {
throw new Error(`AWF_CLAUDE_HOSTED_WEB_POLICY.domains contains an invalid domain: ${JSON.stringify(domain)}`);
}
if (!domains.includes(domain)) domains.push(domain);
}

const policy = { enabled: true, mode: parsed.mode, domains };

if (parsed.maxUses !== undefined) {
if (!Number.isInteger(parsed.maxUses) || parsed.maxUses < 1) {
throw new Error('AWF_CLAUDE_HOSTED_WEB_POLICY.maxUses must be a positive integer');
}
policy.maxUses = parsed.maxUses;
}

return policy;
return parseHostedWebPolicy(raw, 'AWF_CLAUDE_HOSTED_WEB_POLICY');
}

/**
Expand Down
Loading
Loading