Runtime Network Permissions
Overview
Code-Mode now supports runtime network permissions via elicitations. When code tries to access a restricted network (private IPs, localhost), the user is prompted for permission. This makes local network MCPs practical while maintaining security.
The Problem
Phase 4 network isolation blocks private IPs (192.168.x.x) by default for security. But local network MCPs need access to devices:
- LG TV at 192.168.1.100
- Philips Hue at 192.168.1.50
- Home Assistant at homeassistant.local:8123
- Dev servers at localhost:3000
The Solution
When restricted network access is attempted, show a permission dialog asking the user:
┌──────────────────────────────────────┐
│ Network Access Permission │
├──────────────────────────────────────┤
│ LG Remote wants to access │
│ local network (private IP): │
│ │
│ http://192.168.1.100:3000/power-on │
│ │
│ Allow this network access? │
│ │
│ [Allow Once] [Allow Always] [Deny] │
└──────────────────────────────────────┘User decides:
- Allow Once: Permission for 1 hour (temporary)
- Allow Always: Permission forever (trusted device)
- Deny: Block access
Real-World Example: LG Remote
Scenario
User asks: "Turn on my TV and set volume to 15"
AI generates code:
// Check TV status
const status = await fetch('http://192.168.1.100:3000/status');
const data = await status.json();
// Turn on if off
if (data.power === 'off') {
await fetch('http://192.168.1.100:3000/power-on', { method: 'POST' });
}
// Set volume
await fetch('http://192.168.1.100:3000/volume', {
method: 'POST',
body: JSON.stringify({ level: 15 })
});What Happens
First Request (/status):
- Code tries:
fetch('http://192.168.1.100:3000/status') - NetworkPolicyManager: ❌ Private IP (blocked by default)
- Elicitation shown:
Worker Code wants to access local network (private IP): http://192.168.1.100:3000/status Allow this network access? [Allow Once] [Allow Always] [Deny] - User clicks: [Allow Always] ← Trust this TV
- Permission cached permanently
- Request proceeds → TV returns status
Second Request (/power-on):
- Code tries:
fetch('http://192.168.1.100:3000/power-on') - NetworkPolicyManager checks cache: ✅ Approved (same URL)
- No prompt - uses cached permission
- Request proceeds → TV powers on
Third Request (/volume):
- Code tries:
fetch('http://192.168.1.100:3000/volume') - NetworkPolicyManager checks cache: ✅ Approved (same URL)
- No prompt - uses cached permission
- Request proceeds → Volume set
Result: User prompted once, subsequent requests work automatically!
Permission Types
1. Allow Once (Temporary)
- Duration: 1 hour
- Use case: Testing, one-time access, untrusted networks
- Example: Accessing a friend's dev server
User clicks: [Allow Once]
└→ Permission expires after 1 hour
└→ Next request after expiry: shows prompt again2. Allow Always (Permanent)
- Duration: Forever (until manually revoked)
- Use case: Trusted devices, home network, local development
- Example: Your LG TV, Philips Hue, Home Assistant
User clicks: [Allow Always]
└→ Permission cached permanently
└→ All future requests: automatic (no prompts)
└→ Can revoke anytime via networkPolicy.revokePermission()3. Deny (Block)
- Duration: Blocked permanently
- Result: Request fails with error
- Example: Suspicious or unauthorized access
User clicks: [Deny]
└→ Request blocked
└→ Error: "Network request blocked: User denied permission"Permission Management
List Permissions
const permissions = networkPolicyManager.getPermissions();
// Output:
[
{
url: 'http://192.168.1.100:3000/status',
approved: true,
permanent: true // "Allow Always"
},
{
url: 'http://localhost:3000/api',
approved: true,
permanent: false // "Allow Once" (expires in 1h)
}
]Revoke Permission
// Revoke specific URL
networkPolicyManager.revokePermission('http://192.168.1.100:3000/status');
// Next request to that URL: shows prompt againClear All Permissions
// Clear all cached permissions
networkPolicyManager.clearPermissions();
// All future restricted requests: show promptsImplementation
Setup (Main Thread)
import { NetworkPolicyManager, SECURE_NETWORK_POLICY } from './network-policy.js';
import { CodeExecutor } from './code-executor.js';
// Create elicitation function
const elicitationFunction = async (params) => {
// If client supports elicitations (Claude Desktop)
if (supportsElicitations) {
return await showUIElicitation(params);
}
// Fallback to system dialog
return await showSystemDialog(params);
};
// Create network policy with elicitations
const networkPolicy = new NetworkPolicyManager(
SECURE_NETWORK_POLICY, // Default: block private IPs
elicitationFunction // Show permission prompts
);
// Create code executor
const executor = new CodeExecutor(
toolsProvider,
toolExecutor,
undefined,
bindingsManager,
networkPolicy // With elicitation support
);User Code (Sandbox)
// User code - no changes needed!
const response = await fetch('http://192.168.1.100:3000/status');
// First time: permission prompt shown
// Second time: uses cached permissionSecurity Guarantees
✅ User sees exactly what's being accessed
- Full URL shown in elicitation
- Access type clearly labeled (localhost, private IP, etc.)
✅ User controls all network access
- Every restricted access requires approval
- Can deny suspicious requests
✅ Principle of least privilege
- Only approved URLs work
- Can revoke permissions anytime
✅ Audit trail
- All permissions logged
- Can list all approved URLs
✅ No blanket permissions
- More secure than
allowPrivateIPs: true - Each URL requires separate approval
Benefits Over Static Policies
| Approach | Security | Flexibility | User Experience |
|---|---|---|---|
| Block all | ✅ Secure | ❌ Inflexible | ❌ Breaks local MCPs |
| Allow all | ❌ Insecure | ✅ Flexible | ✅ Works, but risky |
| Static policies | ✅ Secure | ⚠️ Pre-configured | ⚠️ Complex setup |
| Runtime permissions | ✅ Secure | ✅ Flexible | ✅ Simple & transparent |
Elicitation Format
The elicitation message includes:
Requester: Who is requesting access
- "Worker Code" (direct fetch from sandbox)
- "LG Remote" (from binding, if supported)
Access Type: What kind of network
- "localhost" (127.0.0.1, ::1)
- "local network (private IP)" (192.168.x.x, 10.x.x.x)
- "external domain" (public internet)
Full URL: Exact URL being accessed
http://192.168.1.100:3000/power-on- Complete transparency
Clear Options:
- Allow Once - Temporary (1 hour)
- Allow Always - Permanent
- Deny - Block
Use Cases
1. LG TV Remote
// User: "Turn on my TV"
await fetch('http://192.168.1.100:3000/power-on');
// Prompt: "...wants to access local network: http://192.168.1.100..."
// User: [Allow Always] ← Trust this TV
// Future TV commands: automatic2. Philips Hue Lights
// User: "Set living room lights to blue"
await fetch('http://192.168.1.50/api/lights/1/state', {
method: 'PUT',
body: JSON.stringify({ on: true, hue: 46920 })
});
// Prompt once → [Allow Always] → All light controls work3. Home Assistant
// User: "Turn off all lights"
await fetch('http://homeassistant.local:8123/api/services/light/turn_off', {
method: 'POST',
headers: { 'Authorization': 'Bearer ...' }
});
// Prompt once → [Allow Always] → All home automation works4. Local Dev Server (Temporary)
// User: "Check my dev server status"
await fetch('http://localhost:3000/api/health');
// Prompt: "...wants to access localhost..."
// User: [Allow Once] ← Only for this debugging session
// Permission expires after 1 hourSummary
Runtime network permissions via elicitations make Code-Mode practical for real-world use cases involving local devices while maintaining enterprise-grade security through informed user consent.
- 🔒 Secure: User controls all restricted network access
- 🎯 Flexible: Works with any local network device
- 👤 Transparent: User sees exactly what's accessed
- 🚀 Practical: LG Remote, Philips Hue, Home Assistant all work
- ⚡ Efficient: Permissions cached (user not spammed)
- 🔧 Manageable: Can revoke/clear permissions anytime