Rust core Bash::builder() already supports outbound HTTP config via network, credential, credential_placeholder, http_handler, before_http, after_http, and bot_auth. Python bindings expose none of that today, and the Python extension currently compiles bashkit without http_client or bot-auth.
There's been recent Python parity work for snapshotting, direct VFS helpers, and shell state. Network/HTTP config is still a clear gap.
Summary
- Enable Rust HTTP support in the Python extension.
- Add a single
network= kwarg on both Bash(...) and BashTool(...).
- The full Python surface should cover Rust parity for allowlists,
allow_all, credential injection, credential placeholders, optional transport hooks, and bot-auth.
- Network should stay disabled by default, preserve config across
reset(), and be reattachable via from_snapshot(...).
Proposed API
Expose one structured network= kwarg on both Bash(...) and BashTool(...).
bash = Bash(
network={
"allow": ["https://api.github.com", "https://api.openai.com/v1"],
"block_private_ips": True,
"credentials": [{"pattern": "https://api.github.com", "kind": "bearer", "token": "ghp_test"}],
"credential_placeholders": [{"env": "OPENAI_API_KEY", "pattern": "https://api.openai.com", "kind": "bearer", "token": "sk-real"}],
"before_http": [audit_request],
"after_http": [audit_response],
"http_handler": mock_transport,
"bot_auth": {"seed": "base64url-ed25519-seed", "agent_fqdn": "bot.example.com", "validity_secs": 300},
},
)
tool = BashTool(network={"allow_all": True})
Rust-parity semantics should be:
- omitted
network means network disabled
network={"allow": []} means explicit empty allowlist and blocks all URLs
network={"allow_all": True} maps to NetworkAllowlist::allow_all()
- wildcard patterns like
"*" are not part of allowlist semantics
block_private_ips defaults to True
- injected credentials overwrite conflicting script-provided headers
http_handler runs after allowlist and private-IP checks so the security boundary stays in bashkit
Detailed typing sketch and design notes
Runtime surface should stay plain dicts + callables. Python stubs can expose TypedDict / Protocol aliases for better typing.
from typing import Awaitable, Literal, Optional, Protocol, TypedDict, Union
from typing_extensions import NotRequired
HeaderList = list[tuple[str, str]]
class HttpResponse(TypedDict):
status: int
headers: HeaderList
body: bytes
class HttpRequestEvent(TypedDict):
method: str
url: str
headers: HeaderList
class HttpResponseEvent(TypedDict):
url: str
status: int
headers: HeaderList
class CancelHttpRequest(TypedDict):
action: Literal["cancel"]
reason: str
class HttpHandler(Protocol):
def __call__(
self,
method: str,
url: str,
body: Optional[bytes],
headers: HeaderList,
) -> Union[HttpResponse, Awaitable[HttpResponse]]: ...
class BeforeHttpHook(Protocol):
def __call__(
self,
event: HttpRequestEvent,
) -> Union[
HttpRequestEvent,
CancelHttpRequest,
Awaitable[Union[HttpRequestEvent, CancelHttpRequest]],
]: ...
class AfterHttpHook(Protocol):
def __call__(
self,
event: HttpResponseEvent,
) -> Union[
None,
HttpResponseEvent,
Awaitable[Union[None, HttpResponseEvent]],
]: ...
class BearerCredentialInjection(TypedDict):
pattern: str
kind: Literal["bearer"]
token: str
class HeaderCredentialInjection(TypedDict):
pattern: str
kind: Literal["header"]
name: str
value: str
class HeadersCredentialInjection(TypedDict):
pattern: str
kind: Literal["headers"]
headers: HeaderList
CredentialInjection = Union[
BearerCredentialInjection,
HeaderCredentialInjection,
HeadersCredentialInjection,
]
class BearerCredentialPlaceholder(TypedDict):
env: str
pattern: str
kind: Literal["bearer"]
token: str
class HeaderCredentialPlaceholder(TypedDict):
env: str
pattern: str
kind: Literal["header"]
name: str
value: str
class HeadersCredentialPlaceholder(TypedDict):
env: str
pattern: str
kind: Literal["headers"]
headers: HeaderList
CredentialPlaceholder = Union[
BearerCredentialPlaceholder,
HeaderCredentialPlaceholder,
HeadersCredentialPlaceholder,
]
class BotAuthConfig(TypedDict):
seed: Union[str, bytes]
agent_fqdn: NotRequired[str]
validity_secs: NotRequired[int]
class AllowlistNetworkConfig(TypedDict):
allow: list[str]
block_private_ips: NotRequired[bool]
credentials: NotRequired[list[CredentialInjection]]
credential_placeholders: NotRequired[list[CredentialPlaceholder]]
http_handler: NotRequired[HttpHandler]
before_http: NotRequired[list[BeforeHttpHook]]
after_http: NotRequired[list[AfterHttpHook]]
bot_auth: NotRequired[BotAuthConfig]
class AllowAllNetworkConfig(TypedDict):
allow_all: Literal[True]
block_private_ips: NotRequired[bool]
credentials: NotRequired[list[CredentialInjection]]
credential_placeholders: NotRequired[list[CredentialPlaceholder]]
http_handler: NotRequired[HttpHandler]
before_http: NotRequired[list[BeforeHttpHook]]
after_http: NotRequired[list[AfterHttpHook]]
bot_auth: NotRequired[BotAuthConfig]
NetworkConfig = Union[AllowlistNetworkConfig, AllowAllNetworkConfig]
Additional notes:
- Callback surfaces should follow existing Python callback conventions used by
custom_builtins and ScriptedTool.add_tool.
- Sync and async callables should both work.
- Callback ordering should follow list order.
- Callbacks should not be allowed to re-enter the same
Bash / BashTool instance via live execution methods.
- Snapshots should not serialize live callbacks or secret material; those should be reattached via constructor /
from_snapshot(...) kwargs.
- Today the Python crate omits
http_client, exposes no network= kwarg, and reset() / from_snapshot() have no network config to preserve.
crates/bashkit-python/README.md currently advertises curl, but the Python package cannot enable allowlisted HTTP today.
Recommended rollout
- Enable
http_client and ship network={"allow": ...} / network={"allow_all": True} with block_private_ips, reset(), and from_snapshot() support.
- Add
credentials and credential_placeholders.
- Add Python callback support for
http_handler, before_http, and after_http.
- Add
bot_auth support and enable the corresponding Rust feature for the Python crate.
The issue should describe the full end-state API above even if implementation lands in that order.
Acceptance criteria
Rust core
Bash::builder()already supports outbound HTTP config vianetwork,credential,credential_placeholder,http_handler,before_http,after_http, andbot_auth. Python bindings expose none of that today, and the Python extension currently compilesbashkitwithouthttp_clientorbot-auth.There's been recent Python parity work for snapshotting, direct VFS helpers, and shell state. Network/HTTP config is still a clear gap.
Summary
network=kwarg on bothBash(...)andBashTool(...).allow_all, credential injection, credential placeholders, optional transport hooks, and bot-auth.reset(), and be reattachable viafrom_snapshot(...).Proposed API
Expose one structured
network=kwarg on bothBash(...)andBashTool(...).Rust-parity semantics should be:
networkmeans network disablednetwork={"allow": []}means explicit empty allowlist and blocks all URLsnetwork={"allow_all": True}maps toNetworkAllowlist::allow_all()"*"are not part of allowlist semanticsblock_private_ipsdefaults toTruehttp_handlerruns after allowlist and private-IP checks so the security boundary stays in bashkitDetailed typing sketch and design notes
Runtime surface should stay plain dicts + callables. Python stubs can expose
TypedDict/Protocolaliases for better typing.Additional notes:
custom_builtinsandScriptedTool.add_tool.Bash/BashToolinstance via live execution methods.from_snapshot(...)kwargs.http_client, exposes nonetwork=kwarg, andreset()/from_snapshot()have no network config to preserve.crates/bashkit-python/README.mdcurrently advertisescurl, but the Python package cannot enable allowlisted HTTP today.Recommended rollout
http_clientand shipnetwork={"allow": ...}/network={"allow_all": True}withblock_private_ips,reset(), andfrom_snapshot()support.credentialsandcredential_placeholders.http_handler,before_http, andafter_http.bot_authsupport and enable the corresponding Rust feature for the Python crate.The issue should describe the full end-state API above even if implementation lands in that order.
Acceptance criteria
http_clientBash(...)acceptsnetwork=...BashTool(...)accepts the samenetwork=...runtime configNetworkAllowlist::allow_all()network={"allow": []}blocks all, distinct from omittednetwork)curl/wget/httpwork from Python when the target URL is allowlistedhttp_handler,before_http, andafter_httpbot_authconfig parityreset()preserves original network config on bothBashandBashToolfrom_snapshot()acceptsnetwork=...and restores a configured instance