main
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
144e322391 |
Add --publish: the one ingress control
Everything else in the file gates egress, which left an everyday request unanswered: the agent starts a dev server inside the sandbox and a human wants to look at it. This puts a listener on the host that forwards into the container, so a browser, curl or an API tester reaches the server the sandboxed process runs. `--publish [ADDR:]HOST:CONTAINER`, config `publish` and AISB_PUBLISH, repeatable, with the cost named in the flag: it is ingress, so whatever that server serves is reachable from the address it binds. The default address is 127.0.0.1 because a bare podman -p binds every interface (measured) -- a port committed in a repo config must not quietly be a port on the LAN. publish_addr changes the default machine-wide; a spec with a non-loopback address warns on every run. The grammar is smaller than podman's on purpose: one port per entry, no ranges, no /udp, no `*` -- this list is reviewed line by line, and typing 0.0.0.0 is the whole warning. An address means three parts, so `127.0.0.1:8000` is refused rather than guessed. No -p alias: that flag belongs to the harness (claude -p prints). Fail closed, as elsewhere: --net none cannot forward a port and podman discards the mapping while starting the container anyway, so "it ran" would mean "nothing is listening"; a host port already in use is named before the container starts, not after a build; pick_port() will not hand the gate a port this run publishes. The container is labelled with the ports it was given, and doctor reports them. Inside, publish_rules() accepts tcp on the published ports in the input chain and never the output one. Measured (docs/networking.md): pasta delivers the connection over loopback, so that rule counts 0 packets on the reference host; it is there for a host or a bridge network that delivers on the interface, and so that `nft list table inet aisb` answers "what can reach this container". The ports are validated again inside the entrypoint, because a ruleset is not somewhere to splice an unvalidated string. And the measurement that makes a configured preview look broken: a server bound to 127.0.0.1 inside answers inside and refuses the published port outside, so dev servers must bind 0.0.0.0 -- both skills say so in the change that writes the port. Tests. Unit: the grammar and every port list that looks right and is not, config/env/flag precedence and dedup, the canonical spec in --print-policy, the `# ingress:` header, the flags reaching the invocation before the image, egress enforcement byte-identical with a port open, which chain the rule lands in and that a non-numeric port never reaches nft as code. e2e (tests/e2e/13_publish.sh): one real container with enforce=nft where the host gets its 200 and the same run still refuses a domain outside the allowlist at 403, the server runs as uid 1000 with no capabilities so it cannot widen its own port, and the host listener is gone when the container exits. LLM-Generated: true |
||
|
|
a91744f390 |
stop the driver hanging on a terminal and on a launcher
ai-sandbox-autophase could sit forever, printing nothing, in two places that are both before the first unit -- which is why both looked like a script that "does nothing". The rc probe ran `timeout 15 bash -ic` in front of the user's terminal. An interactive bash calls tcsetpgrp() to take that terminal, is in its background, and is stopped for trying; a stopped process does not take SIGTERM, so the timeout waited in front of it and the driver waited on the command substitution behind it. It now runs in its own session (setsid), with stdin closed, under `timeout -k`, and reads the definition from a temp file rather than a pipe, because a grandchild an rc file leaves behind holds a pipe's EOF open long after the shell it came from has been reaped. A launcher is called *with* the run, and a function ending at `--env NAME` -- the shape everyone writes for typing by hand, including every example this repo printed -- discards the flags and the in-container command. The container then starts the image's default harness with no prompt and, with --no-interactive gone, the terminal attached: an agent waiting for keystrokes behind a pipe nobody reads. `ap_forwards_args` checks the captured definition, --status and the preflight name it with the one-line fix, and every child now starts with stdin on /dev/null so a launcher cannot put the sandbox back on the keyboard. Aliases and whole command lines need no forwarding: the shell appends the words to those. Tests: a launcher fixture that drops its arguments, one that leaves a process behind, and shape assertions on ap_probe_rc. LLM-Generated: true |
||
|
|
209f167e43 |
add doc-sandbox: answer from the installed program, not memory
The explainer for questions about a flag, a config key, a refusal, a mechanism or a security boundary. ai-sandbox generates its own Containerfile, entrypoint, squid config and netfilter rules from one script, so the text of the program is the documentation of the version the user will run: the skill sets a strict ground-truth order -- the installed script and --help, then the repo docs, then the tests as the executable spec -- and its own fact sheet last, overridden by all of them. scripts/locate.sh resolves that order on an arbitrary machine: the installed script and its version, the source tree behind it (via $AISB_REPO, the script's own parent, $PWD, the git root), the docs with their line counts, and the config/state/cache locations. references/source-map.md maps a question topic to the function, doc and test that hold the answer, plus the vocabulary (split_target, classify_allows, gate vs --no-gate vs --proxy, POL[]/P_*/OPT_*) and the FAQ. The skill names which safe surface answers what: the --dry-run, --print-entrypoint, --print-containerfile and --print-policy previews build nothing and reach no registry, while inspect, doctor --deep, build, update and gate up do change the machine and are not for an explanation. It records the two honest exceptions rather than overclaiming -- --print-policy ensures the home volume, and --no-gate hits DNS. Read-only by construction: no build, no container, no network call, and it reads pi_config_bridge() rather than a models.json holding a live key. Its worst failure mode is an invented flag, so the answer format requires the real spelling, the source it came from, what the thing does not do, and a plain "no" plus the closest real thing when the answer is no. LLM-Generated: true |
||
|
|
34f94e38be |
add right-sandbox: write a project's own ai-sandbox policy
Turns a project directory into that project's policy, so a plain `ai-sandbox` in the directory is the correct, reviewable invocation. It writes the two files load_config() actually reads -- .ai-sandbox/allow and .ai-sandbox/config -- and nothing else: no sandbox.yaml, no local override, no invented schema, because a file nobody reads is a policy nobody has. scripts/survey.sh gathers the evidence read-only and prints no values: variable NAMES, remote and registry HOSTS, service PORTS, with URL credentials stripped before anything is printed -- the same rule --dry-run, --print-policy and the session log hold themselves to. The agent decides from that evidence, then verifies with --dry-run and --print-policy rather than assuming an allowlist entry means a working toolchain. references/settings-map.md maps every accepted key and flag to its default, plus per-ecosystem allowlists and the survey-to-policy cheat sheet. references/port-and-loopback.md is the honest answer on ports: outbound to a service on the machine via the mapped loopback address 100.64.0.1 (needs an allow entry and a client that does not hand the request to the egress proxy), and why a port the sandbox listens on is not reachable from the host in --net allow -- with probes, because that half is a reading of gen_rules(), not a measurement. Policy files only. Never builds, never starts a container, and never writes a real credential: it writes .ai-sandbox/env.example and tells the user what to put in the gitignored 0600 file. LLM-Generated: true |