Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cdedbb7ca |
@@ -217,7 +217,7 @@ class ClaudeAgentProvider(AgentProvider):
|
|||||||
if not agent.skills:
|
if not agent.skills:
|
||||||
return
|
return
|
||||||
skills_dir = _skills_dir(plan.guest_home)
|
skills_dir = _skills_dir(plan.guest_home)
|
||||||
bottle.exec(f"mkdir -p {shlex.quote(skills_dir)}", user="root")
|
bottle.exec(f"mkdir -p {skills_dir}", user="root")
|
||||||
for name in agent.skills:
|
for name in agent.skills:
|
||||||
src = host_skill_dir(name)
|
src = host_skill_dir(name)
|
||||||
if not os.path.isdir(src):
|
if not os.path.isdir(src):
|
||||||
@@ -227,13 +227,9 @@ class ClaudeAgentProvider(AgentProvider):
|
|||||||
)
|
)
|
||||||
dst = f"{skills_dir}/{name}"
|
dst = f"{skills_dir}/{name}"
|
||||||
info(f"copying skill {name} into {bottle.name}:{dst}")
|
info(f"copying skill {name} into {bottle.name}:{dst}")
|
||||||
# Defense in depth: skill names are validated kebab-case at
|
bottle.exec(f"rm -rf {dst} && mkdir -p {dst}", user="root")
|
||||||
# manifest load, but quote the path so a future unvalidated
|
|
||||||
# field can't inject shell metacharacters here either.
|
|
||||||
dst_q = shlex.quote(dst)
|
|
||||||
bottle.exec(f"rm -rf {dst_q} && mkdir -p {dst_q}", user="root")
|
|
||||||
bottle.cp_in(f"{src}/.", f"{dst}/")
|
bottle.cp_in(f"{src}/.", f"{dst}/")
|
||||||
bottle.exec(f"chown -R node:node {dst_q}", user="root")
|
bottle.exec(f"chown -R node:node {dst}", user="root")
|
||||||
|
|
||||||
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
||||||
"""Copy the prompt file into the guest, fix ownership/mode.
|
"""Copy the prompt file into the guest, fix ownership/mode.
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class CodexAgentProvider(AgentProvider):
|
|||||||
if not agent.skills:
|
if not agent.skills:
|
||||||
return
|
return
|
||||||
skills_dir = _skills_dir(plan.guest_home)
|
skills_dir = _skills_dir(plan.guest_home)
|
||||||
bottle.exec(f"mkdir -p {shlex.quote(skills_dir)}", user="root")
|
bottle.exec(f"mkdir -p {skills_dir}", user="root")
|
||||||
for name in agent.skills:
|
for name in agent.skills:
|
||||||
src = host_skill_dir(name)
|
src = host_skill_dir(name)
|
||||||
if not os.path.isdir(src):
|
if not os.path.isdir(src):
|
||||||
@@ -193,13 +193,9 @@ class CodexAgentProvider(AgentProvider):
|
|||||||
)
|
)
|
||||||
dst = f"{skills_dir}/{name}"
|
dst = f"{skills_dir}/{name}"
|
||||||
info(f"copying skill {name} into {bottle.name}:{dst}")
|
info(f"copying skill {name} into {bottle.name}:{dst}")
|
||||||
# Defense in depth: skill names are validated kebab-case at
|
bottle.exec(f"rm -rf {dst} && mkdir -p {dst}", user="root")
|
||||||
# manifest load, but quote the path so a future unvalidated
|
|
||||||
# field can't inject shell metacharacters here either.
|
|
||||||
dst_q = shlex.quote(dst)
|
|
||||||
bottle.exec(f"rm -rf {dst_q} && mkdir -p {dst_q}", user="root")
|
|
||||||
bottle.cp_in(f"{src}/.", f"{dst}/")
|
bottle.cp_in(f"{src}/.", f"{dst}/")
|
||||||
bottle.exec(f"chown -R node:node {dst_q}", user="root")
|
bottle.exec(f"chown -R node:node {dst}", user="root")
|
||||||
|
|
||||||
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
||||||
"""Copy the prompt file into the guest, fix ownership/mode.
|
"""Copy the prompt file into the guest, fix ownership/mode.
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ class PiAgentProvider(AgentProvider):
|
|||||||
if not agent.skills:
|
if not agent.skills:
|
||||||
return
|
return
|
||||||
skills_dir = _skills_dir(plan.guest_home)
|
skills_dir = _skills_dir(plan.guest_home)
|
||||||
bottle.exec(f"mkdir -p {shlex.quote(skills_dir)}", user="root")
|
bottle.exec(f"mkdir -p {skills_dir}", user="root")
|
||||||
for name in agent.skills:
|
for name in agent.skills:
|
||||||
src = host_skill_dir(name)
|
src = host_skill_dir(name)
|
||||||
if not os.path.isdir(src):
|
if not os.path.isdir(src):
|
||||||
@@ -248,13 +248,9 @@ class PiAgentProvider(AgentProvider):
|
|||||||
)
|
)
|
||||||
dst = f"{skills_dir}/{name}"
|
dst = f"{skills_dir}/{name}"
|
||||||
info(f"copying skill {name} into {bottle.name}:{dst}")
|
info(f"copying skill {name} into {bottle.name}:{dst}")
|
||||||
# Defense in depth: skill names are validated kebab-case at
|
bottle.exec(f"rm -rf {dst} && mkdir -p {dst}", user="root")
|
||||||
# manifest load, but quote the path so a future unvalidated
|
|
||||||
# field can't inject shell metacharacters here either.
|
|
||||||
dst_q = shlex.quote(dst)
|
|
||||||
bottle.exec(f"rm -rf {dst_q} && mkdir -p {dst_q}", user="root")
|
|
||||||
bottle.cp_in(f"{src}/.", f"{dst}/")
|
bottle.cp_in(f"{src}/.", f"{dst}/")
|
||||||
bottle.exec(f"chown -R node:node {dst_q}", user="root")
|
bottle.exec(f"chown -R node:node {dst}", user="root")
|
||||||
|
|
||||||
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
||||||
prompt_path = _prompt_path(plan.guest_home)
|
prompt_path = _prompt_path(plan.guest_home)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import cast
|
|||||||
from .agent_provider import PROVIDER_TEMPLATES
|
from .agent_provider import PROVIDER_TEMPLATES
|
||||||
from .manifest_util import ManifestError, as_json_object
|
from .manifest_util import ManifestError, as_json_object
|
||||||
from .manifest_git import ManifestGitUser
|
from .manifest_git import ManifestGitUser
|
||||||
from .manifest_schema import AGENT_MODEL_KEYS, is_valid_entity_name
|
from .manifest_schema import AGENT_MODEL_KEYS
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -161,16 +161,6 @@ class ManifestAgent:
|
|||||||
f"agent '{name}' skills[{i}] must be a string "
|
f"agent '{name}' skills[{i}] must be a string "
|
||||||
f"(was {type(skill).__name__})"
|
f"(was {type(skill).__name__})"
|
||||||
)
|
)
|
||||||
# Skill names become host/guest path segments and are
|
|
||||||
# interpolated into provisioning shell commands, so they
|
|
||||||
# must fit the same kebab-case convention as bottle/agent
|
|
||||||
# filenames — rejecting anything that could break out of a
|
|
||||||
# path segment or inject shell metacharacters.
|
|
||||||
if not is_valid_entity_name(skill):
|
|
||||||
raise ManifestError(
|
|
||||||
f"agent '{name}' skills[{i}] {skill!r} is not a valid "
|
|
||||||
f"skill name; must match [a-z][a-z0-9-]*"
|
|
||||||
)
|
|
||||||
collected.append(skill)
|
collected.append(skill)
|
||||||
skills = tuple(collected)
|
skills = tuple(collected)
|
||||||
|
|
||||||
|
|||||||
@@ -33,20 +33,13 @@ AGENT_KEYS = (
|
|||||||
AGENT_MODEL_KEYS = AGENT_KEYS | frozenset({"prompt"})
|
AGENT_MODEL_KEYS = AGENT_KEYS | frozenset({"prompt"})
|
||||||
|
|
||||||
|
|
||||||
def is_valid_entity_name(name: str) -> bool:
|
|
||||||
"""True if `name` fits the kebab-case `[a-z][a-z0-9-]*` convention
|
|
||||||
shared by bottle/agent filenames and skill names. Names that satisfy
|
|
||||||
this are also safe to interpolate into a host/guest path segment."""
|
|
||||||
return bool(_FILENAME_RX.match(name))
|
|
||||||
|
|
||||||
|
|
||||||
def entity_name_from_path(path: Path) -> str | None:
|
def entity_name_from_path(path: Path) -> str | None:
|
||||||
"""Return the entity name implied by the filename, or None if the
|
"""Return the entity name implied by the filename, or None if the
|
||||||
filename does not fit the [a-z][a-z0-9-]* convention."""
|
filename does not fit the [a-z][a-z0-9-]* convention."""
|
||||||
if path.suffix != ".md":
|
if path.suffix != ".md":
|
||||||
return None
|
return None
|
||||||
stem = path.stem
|
stem = path.stem
|
||||||
if not is_valid_entity_name(stem):
|
if not _FILENAME_RX.match(stem):
|
||||||
return None
|
return None
|
||||||
return stem
|
return stem
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# PRD prd-new: Egress control plane — metering, budgets, and forced cutoff
|
||||||
|
|
||||||
|
- **Status:** Draft
|
||||||
|
- **Author:** didericis
|
||||||
|
- **Created:** 2026-06-25
|
||||||
|
- **Issue:** #251
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add an **out-of-band egress enforcement & observability plane**: meter every
|
||||||
|
agent's token usage at the egress proxy, decrement budgets without the agent's
|
||||||
|
cooperation, and forcibly cut a bottle's egress when a budget is exhausted —
|
||||||
|
either automatically or on command from a host-level dashboard. The trigger
|
||||||
|
(usage threshold) and the action (route-drop / freeze / kill) both live in the
|
||||||
|
egress plane and run with no agent in the loop. This is distinct from the
|
||||||
|
supervise sidecar (PRD 0013), which is agent-initiated and therefore cannot
|
||||||
|
enforce a cost cutoff on a runaway agent. State (usage ledger, budgets, audit)
|
||||||
|
moves into a host-level SQLite database behind a thin repository API, the first
|
||||||
|
SQL store in an otherwise flat-file repo.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
bot-bottle can't currently do two things the cost-overrun case demands:
|
||||||
|
|
||||||
|
1. **Forced egress shutdown on limit.** When an agent crosses a token
|
||||||
|
threshold, kill its egress automatically — no human in the loop.
|
||||||
|
2. **Remote (host-level) management.** Drive agents from a single surface:
|
||||||
|
see usage, cut egress, stop bottles, to prevent cost overruns.
|
||||||
|
|
||||||
|
The existing supervise sidecar (PRD 0013) is **entirely agent-initiated**: every
|
||||||
|
action begins with the agent voluntarily calling an MCP tool and an operator
|
||||||
|
approving it. A runaway or expensive agent — exactly the cost-overrun case —
|
||||||
|
will never call `egress-block` on itself. Supervision is therefore a
|
||||||
|
**collaborative recovery** mechanism, not an **enforcement** mechanism; making
|
||||||
|
it mandatory (#249) would not deliver forced cost-cutoff.
|
||||||
|
|
||||||
|
The requirement forces a distinction the current design blurs:
|
||||||
|
|
||||||
|
- **Plane A — enforcement / observability (this PRD).** System → infrastructure.
|
||||||
|
Meter usage, cut egress on threshold or command, account for cost.
|
||||||
|
Out-of-band; independent of the agent. **Unconditional** — an enforcement
|
||||||
|
plane you can opt out of isn't enforcement.
|
||||||
|
- **Plane B — agent-facing recovery (the existing supervise sidecar).**
|
||||||
|
Agent → operator, approval-gated. Useful interactively; meaningless for a
|
||||||
|
headless agent with no operator watching its queue. Remains optional.
|
||||||
|
|
||||||
|
This PRD builds Plane A. It reframes the "always-on control" invariant of #249
|
||||||
|
as "the egress control plane is always present" — a more defensible property
|
||||||
|
than "every agent runs the agent-facing supervisor." Unsupervised
|
||||||
|
(headless/CI/ephemeral) agents stay first-class: still subject to the mandatory
|
||||||
|
meter + kill switch, they simply lack the agent-facing proposal tools they
|
||||||
|
couldn't use anyway.
|
||||||
|
|
||||||
|
## Goals / Success Criteria
|
||||||
|
|
||||||
|
- The egress proxy meters every request to a metered API host (e.g.
|
||||||
|
`api.anthropic.com`) and records authoritative token usage per bottle and per
|
||||||
|
agent provider, with no agent cooperation.
|
||||||
|
- A budget can be set at four scopes with deterministic precedence
|
||||||
|
(**agent → bottle → parent bottle → global host budget**); the
|
||||||
|
most-specific applicable budget governs.
|
||||||
|
- When usage crosses a budget, the bottle's configured **cutoff policy**
|
||||||
|
(`cutoff` | `freeze` | `kill`) fires automatically, executed host-side on the
|
||||||
|
egress plane — never via the supervise queue.
|
||||||
|
- An operator can, from a single **host-level TUI dashboard**, see live per-bottle
|
||||||
|
usage against budget and command a cutoff/stop on demand.
|
||||||
|
- Host budgets, default cutoff policy, and per-provider limits are declared in a
|
||||||
|
new host-level `~/.bot-bottle/settings.yml`, parseable by `yaml_subset.py`.
|
||||||
|
- All usage, budget state, and enforcement actions persist in a host-level
|
||||||
|
SQLite DB behind a thin repository API, so the store can later be swapped for
|
||||||
|
a cross-host cloud service.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- **Remote control / cross-host control plane.** Web + mobile remote control,
|
||||||
|
cross-host budgets, and the authn/transport they require are explicitly
|
||||||
|
deferred. v1 is a **host-only TUI** with no remote surface.
|
||||||
|
- **Dollar-denominated budgets.** Budgets are token counts keyed by agent
|
||||||
|
provider, not currency. Price tables are out of scope.
|
||||||
|
- **Migrating existing flat-file state into SQLite.** Resume `metadata.json`,
|
||||||
|
transcripts, Dockerfile overrides, the supervise queue, and audit logs stay on
|
||||||
|
the filesystem. Only the *new* metering/budget/enforcement ledger is SQL.
|
||||||
|
- **Making the supervise sidecar (Plane B) mandatory.** Out of scope here; this
|
||||||
|
PRD is the answer to "what should be unconditional" (Plane A), leaving #249's
|
||||||
|
Plane-B question open.
|
||||||
|
- **Per-request hard pre-send blocking as the primary mechanism.** The gate is
|
||||||
|
budget-crossing detected at/after metering; a pre-flight estimator (below) is a
|
||||||
|
refinement, not the core enforcement path.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Two measurements: gate vs. account
|
||||||
|
|
||||||
|
There are two distinct needs, and they want different signals:
|
||||||
|
|
||||||
|
- **Account (authoritative).** Decrement the real budget from the API
|
||||||
|
**response**, which already carries authoritative usage (Anthropic
|
||||||
|
`input_tokens` / `output_tokens`, OpenAI `usage`). The egress addon already
|
||||||
|
has a `response(flow)` hook (`bot_bottle/egress_addon.py:460`), so the real
|
||||||
|
number is available with no extra network call. **Caveat:** agent traffic is
|
||||||
|
mostly streaming SSE, so the response path must tail the stream for the final
|
||||||
|
usage event rather than parse a single JSON body — scoped explicitly as work.
|
||||||
|
- **Gate (estimate).** To block *before* sending, only the request is available,
|
||||||
|
so an estimator / provider `count_tokens` endpoint is the only option.
|
||||||
|
|
||||||
|
Calling `count_tokens` for accounting would be both less accurate *and* an extra
|
||||||
|
metered egress call per request, so accounting uses response `usage` and the
|
||||||
|
estimator is reserved for the optional pre-flight gate.
|
||||||
|
|
||||||
|
### `count_tokens` on agent providers
|
||||||
|
|
||||||
|
Add an abstract `count_tokens(request) -> int` to the `AgentProvider`
|
||||||
|
abstraction (`bot_bottle/agent_provider.py`):
|
||||||
|
|
||||||
|
- **Default** is a good-enough stdlib estimator. Prefer stdlib only; a small
|
||||||
|
pip dependency *for the sidecar* is acceptable for the fallback if stdlib
|
||||||
|
proves too inaccurate (this does not relax the package's stdlib-first stance —
|
||||||
|
it would be a sidecar-only dep, like the bundle already carries).
|
||||||
|
- **Built-in `claude`** uses Anthropic's token-counting endpoint;
|
||||||
|
**built-in `codex`** uses OpenAI's. These are exact for the gate but cost a
|
||||||
|
metered call, so they are gate-only; accounting still comes from the response.
|
||||||
|
|
||||||
|
### Budgets and precedence
|
||||||
|
|
||||||
|
Budgets are token counts keyed by **agent provider name** (the same names
|
||||||
|
bottles already use). Four scopes, most-specific wins:
|
||||||
|
|
||||||
|
```
|
||||||
|
agent → bottle → parent bottle → global (host)
|
||||||
|
```
|
||||||
|
|
||||||
|
The global host budget is the highest-priority feature to ship (the cross-host
|
||||||
|
control plane will eventually consume it); per-agent and per-bottle budgets
|
||||||
|
override it for finer control. A budget can also be supplied **at bottle
|
||||||
|
launch** (`--budget` or equivalent), overriding the settings.yml defaults for
|
||||||
|
that run. Enforcement evaluates the effective budget as the
|
||||||
|
nearest-defined scope at decrement time.
|
||||||
|
|
||||||
|
### `~/.bot-bottle/settings.yml`
|
||||||
|
|
||||||
|
New **host-level** settings file (the `~/.bot-bottle/` root, *not* the per-repo
|
||||||
|
`.bot-bottle/` — host budgets must not be committed per-repo). Parsed by
|
||||||
|
`yaml_subset.py`, so it must stay within that bounded subset (flat mappings,
|
||||||
|
scalars; no anchors, no multi-line block scalars). Shape:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
budget:
|
||||||
|
claude: 5000000 # token budget keyed by agent provider
|
||||||
|
codex: 2000000
|
||||||
|
shutdown: cutoff # default cutoff policy: cutoff | freeze | kill
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forced cutoff and cutoff policy
|
||||||
|
|
||||||
|
On budget exhaustion (or an operator command), the configured per-bottle cutoff
|
||||||
|
policy fires. The three policies map onto primitives that already exist:
|
||||||
|
|
||||||
|
- **`cutoff`** (default) — drop the bottle's `routes.yaml` to empty and reload
|
||||||
|
(or isolate the bottle from the egress network); the agent/bottle keeps
|
||||||
|
running but can no longer reach metered hosts. This is the route-drop already
|
||||||
|
available on the egress plane (`bot_bottle/backend/egress_apply.py`).
|
||||||
|
- **`freeze`** — commit/snapshot state, then kill the agent/bottle; resumable
|
||||||
|
later via `bot_bottle/backend/freeze.py`.
|
||||||
|
- **`kill`** — tear the bottle down without saving state (backend teardown).
|
||||||
|
|
||||||
|
The trigger lives in the metering path and the action in the egress/backend
|
||||||
|
plane; **neither touches the supervise proposal queue** (design constraint from
|
||||||
|
#251).
|
||||||
|
|
||||||
|
### Host-level SQLite store
|
||||||
|
|
||||||
|
**Decision: introduce SQLite now, narrowly.**
|
||||||
|
|
||||||
|
- **The dependency objection doesn't apply.** `sqlite3` is in the Python stdlib,
|
||||||
|
so it does not break the AGENTS.md stdlib-first / no-runtime-pip stance — same
|
||||||
|
category as the hand-rolled `yaml_subset.py`, except the stdlib already ships
|
||||||
|
the whole engine.
|
||||||
|
- **It fits the problem.** A *global* token budget decremented concurrently by N
|
||||||
|
egress sidecars (today `~/.bot-bottle/` already has `state/`, `audit/`,
|
||||||
|
`queue/` written by parallel bottles) is a read-modify-write race. Over JSON
|
||||||
|
that means hand-rolled file locking; SQLite gives atomic transactions + WAL for
|
||||||
|
free. The per-agent/per-bottle precedence rollup plus "sum across all bottles"
|
||||||
|
is a `GROUP BY`, not an N-directory rescan.
|
||||||
|
- **It rehearses the cloud swap.** "Wrap operations in an API so we can swap to a
|
||||||
|
cloud service" maps directly onto a thin repository/DAO over SQLite → Postgres
|
||||||
|
later. A JSON-file store is a worse rehearsal than SQL.
|
||||||
|
|
||||||
|
**Costs (real but bounded):** a new paradigm in a flat-file repo needs a
|
||||||
|
`schema_version` table + idempotent startup migrations; SQLite serializes
|
||||||
|
writers, so WAL mode + `busy_timeout` are required (a non-issue at a handful of
|
||||||
|
bottles); test fixtures need temp DBs.
|
||||||
|
|
||||||
|
**Scope of the store:** one DB at `~/.bot-bottle/bot-bottle.db` behind a thin
|
||||||
|
repository API. Only the **new** metering/budget/enforcement-audit ledger lives
|
||||||
|
there. Existing per-bottle blobs (resume `metadata.json`, transcripts,
|
||||||
|
Dockerfile overrides, supervise queue) stay on the filesystem — migrating them
|
||||||
|
now is churn for no benefit and they lack the concurrency/aggregation problem.
|
||||||
|
|
||||||
|
### Host-level controller + dashboard
|
||||||
|
|
||||||
|
A single **host-level controller** owns the meter, budget evaluation, and the
|
||||||
|
cutoff actions across all bottles (cf. `bot_bottle/cli/supervise.py`'s
|
||||||
|
cross-bottle view), rather than a per-bottle daemon. v1 ships one host-level
|
||||||
|
**TUI dashboard** that reads live usage-vs-budget from the SQLite store and
|
||||||
|
offers on-demand cutoff/stop. The existing supervisor UI should eventually fold
|
||||||
|
into this same dashboard; this PRD lays the host-level surface it will move to.
|
||||||
|
|
||||||
|
## Implementation chunks
|
||||||
|
|
||||||
|
Ordered, individually mergeable:
|
||||||
|
|
||||||
|
1. **SQLite repository foundation.** `~/.bot-bottle/bot-bottle.db`, schema +
|
||||||
|
`schema_version` migrations, WAL + `busy_timeout`, thin repository API,
|
||||||
|
temp-DB test fixtures. No behavior wired yet.
|
||||||
|
2. **Metering at the egress proxy.** Parse authoritative response `usage`
|
||||||
|
(including SSE final-usage tailing) in the egress addon `response` hook;
|
||||||
|
write per-bottle / per-provider usage rows to the ledger.
|
||||||
|
3. **`settings.yml` + budget model.** Host-level `~/.bot-bottle/settings.yml`
|
||||||
|
parsed by `yaml_subset.py`; budget precedence (agent → bottle → parent →
|
||||||
|
global) and the `--budget` launch flag.
|
||||||
|
4. **Forced cutoff + cutoff policy.** Wire the threshold trigger to the
|
||||||
|
`cutoff` / `freeze` / `kill` primitives on the egress/backend plane; record
|
||||||
|
enforcement actions to the audit ledger.
|
||||||
|
5. **Host-level TUI dashboard.** Live usage-vs-budget view + on-demand
|
||||||
|
cutoff/stop, reading the store.
|
||||||
|
6. **`count_tokens` pre-flight gate (optional refinement).** Abstract method +
|
||||||
|
stdlib estimator default; Anthropic/OpenAI endpoints for built-in
|
||||||
|
claude/codex; optional pre-send block.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- **SSE usage tailing robustness.** Buffering streamed responses to extract the
|
||||||
|
final usage event without breaking the agent's own stream consumption — how
|
||||||
|
much of the body must the addon hold, and what's the failure mode if the
|
||||||
|
stream is interrupted mid-flight?
|
||||||
|
- **Crossing mid-request.** A single response can push usage past budget only
|
||||||
|
*after* it's already been delivered. Is post-hoc cutoff (next request blocked)
|
||||||
|
sufficient, or is a pre-flight estimator gate (chunk 6) required for v1?
|
||||||
|
- **Provider name ↔ metered host mapping.** How does the proxy attribute a
|
||||||
|
flow to an agent-provider budget key — by destination host, by bottle
|
||||||
|
identity, or both?
|
||||||
|
- **Parent-bottle budget semantics.** For `bottle extends` (PRD 0025 / 0065)
|
||||||
|
chains, does "parent bottle" mean the manifest parent, the launching bottle,
|
||||||
|
or the full ancestry summed?
|
||||||
|
- **Dashboard ↔ controller transport (even host-only).** In-process, a local
|
||||||
|
socket, or polling the SQLite store directly? Picks the seam the future remote
|
||||||
|
control plane will extend.
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
# Monetization & competitive positioning
|
|
||||||
|
|
||||||
Where, if anywhere, bot-bottle has a paid wedge — given a 2026
|
|
||||||
competitive field that has largely commoditized "sandbox a coding
|
|
||||||
agent." Folds together the agent-provider-agnostic framing, the Fly
|
|
||||||
remote-backend idea, the supervisor/egress-audit play, and the
|
|
||||||
solo-dev/Linux brand instinct, then asks the only question that
|
|
||||||
matters: is there a viable path to revenue that the competition does
|
|
||||||
not already foreclose?
|
|
||||||
|
|
||||||
Companion to
|
|
||||||
[`agent-sandbox-landscape.md`](agent-sandbox-landscape.md) (the
|
|
||||||
isolation-tech survey),
|
|
||||||
[`built-in-supervisor-design.md`](built-in-supervisor-design.md) (the
|
|
||||||
supervise surface this would extend), and
|
|
||||||
[`secret-minimization-over-dlp.md`](secret-minimization-over-dlp.md)
|
|
||||||
(why custody, not detection, is the real moat).
|
|
||||||
|
|
||||||
Market data current as of June 2026.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**Verdict: a path exists, but it is narrow, and it is not the path the
|
|
||||||
project is currently shaped for.** Every individual property bot-bottle
|
|
||||||
leans on — isolation, BYO-image, egress filtering, OSS, self-hosting —
|
|
||||||
is matched by some competitor, and several are now *free* from the agent
|
|
||||||
vendors themselves. There is exactly one defensible position left: the
|
|
||||||
**bundle** that no single competitor occupies —
|
|
||||||
|
|
||||||
> uniform egress audit + secret custody + policy, across *heterogeneous
|
|
||||||
> coding agents you don't trust*, on your infra or a managed pool.
|
|
||||||
|
|
||||||
Monetization is viable **only** if the product is sold as cross-vendor
|
|
||||||
**fleet governance + egress audit for teams**, not as solo-dev agent
|
|
||||||
safety (which the labs give away free). The solo-dev/Linux/anti-corporate
|
|
||||||
energy is real and worth using — but as a *distribution and trust*
|
|
||||||
engine that drives bottom-up adoption into teams, never as the revenue
|
|
||||||
positioning itself. Get those two wires crossed and the business dies:
|
|
||||||
you'd be courting the lowest-willingness-to-pay audience on earth while
|
|
||||||
repelling the only buyer who pays.
|
|
||||||
|
|
||||||
Net: **viable, conditional, and unforgiving of positioning error.** Do
|
|
||||||
Phase 1 (self-hostable egress-audit dashboard) regardless — it's
|
|
||||||
low-risk and it's the demo that makes everything else legible. Gate the
|
|
||||||
go/no-go on whether 5–10 teams confirm they'd pay for cross-vendor
|
|
||||||
egress audit *before* building the hosted tier.
|
|
||||||
|
|
||||||
## The two axes of "agnostic"
|
|
||||||
|
|
||||||
bot-bottle differentiates on two orthogonal axes, and conflating them
|
|
||||||
muddies the pitch:
|
|
||||||
|
|
||||||
1. **Agent-provider agnostic** — run Claude Code, Codex, Aider, a local
|
|
||||||
model, behind one control layer. Already real in the code
|
|
||||||
(`agent_provider.py`, Claude/Codex templates, BYO Dockerfile). This
|
|
||||||
is the axis the labs *structurally cannot* match — Anthropic only
|
|
||||||
runs Claude, OpenAI only their models. Durable.
|
|
||||||
2. **Compute backend** — local (docker / Apple Container / smolmachines)
|
|
||||||
today; a remote **Fly** backend would add a managed pool. This is the
|
|
||||||
axis that makes "fleet" literal for orgs and opens metered billing.
|
|
||||||
Fly is a strong first remote backend because it also subsumes remote
|
|
||||||
spin-up (Machines API) and the tunnel problem (6PN/WireGuard) — but
|
|
||||||
"provider-agnostic compute" should be *earned* after backend #2, not
|
|
||||||
designed up front (premature generalization trap).
|
|
||||||
|
|
||||||
## Competitive field, by capability
|
|
||||||
|
|
||||||
The field doesn't have one competitor; it has a different set on each
|
|
||||||
capability bot-bottle touches. Five dimensions:
|
|
||||||
|
|
||||||
| Capability | Who has it | bot-bottle's standing |
|
|
||||||
| :-- | :-- | :-- |
|
|
||||||
| **Isolation / sandbox** | Anthropic & OpenAI **native, free**; OSS devcontainer wrappers; E2B/Modal/Daytona/Northflank | Commoditized. Not a wedge. |
|
|
||||||
| **Arbitrary BYO Docker image** | Sandbox PaaS (E2B/Modal/Daytona/Northflank) yes; **managed agents: ~none** (Codex = fixed `codex-universal` + setup scripts; Copilot "not supported"; Devin/Jules constrained) | Wedge **vs. managed agents** (structural: it's their infra). Table stakes vs. PaaS. |
|
|
||||||
| **Egress audit + alerts** | LLM-observability tools (Braintrust/Langfuse/Phoenix/Helicone/Datadog) — but on *model calls*, wrong layer. Network-egress security (DeepInspect, AI gateways) — right layer, but decoupled from the agent, not cross-vendor. Sandbox PaaS = gateway/filter, not an audit surface. | **~Nobody in bot-bottle's exact shape** (per-agent egress, tied to the sandbox, with DLP context, cross-vendor). This is the wedge. |
|
|
||||||
| **OSS / self-hosting** | Managed agents: ~none. Sandbox PaaS: ~half (E2B OSS+self-host; Northflank BYOC; Modal closed; **Daytona leaving OSS**). Devcontainer wrappers: ~all. Observability: several. | Real wedge **vs. managed agents only**. Table stakes vs. PaaS, zero differentiation vs. wrappers. |
|
|
||||||
| **Cross-vendor uniformity** | Nobody — the labs won't, PaaS is agent-neutral infra not agent-aware control, wrappers are single-tool | Wedge. The connective tissue of the whole position. |
|
|
||||||
|
|
||||||
The pattern: **isolation and OSS/self-host are commodity; BYO-image and
|
|
||||||
cross-vendor are wedges only against the managed agents; egress-audit in
|
|
||||||
the integrated form is the one thing genuinely unoccupied.**
|
|
||||||
|
|
||||||
## Where bot-bottle is alone vs. where it's table stakes
|
|
||||||
|
|
||||||
- **Alone (the moat):** egress audit + secret custody + policy, *tied to
|
|
||||||
the agent sandbox*, *with DLP context* (which secret, which host,
|
|
||||||
which agent/task), *uniform across vendors*. No competitor bundles
|
|
||||||
these. An enterprise *could* bolt DeepInspect-style egress monitoring
|
|
||||||
onto a sandbox, so the defensibility is the **integration and
|
|
||||||
per-agent context**, not "we can see egress."
|
|
||||||
- **Table stakes (do not lead with these):** "we sandbox agents" (free
|
|
||||||
from the labs), "we're open source" (E2B is; the wrapper crowd all
|
|
||||||
is), "we self-host" (Northflank BYOC, E2B, every wrapper).
|
|
||||||
|
|
||||||
## The two existential competitive facts
|
|
||||||
|
|
||||||
1. **The agent vendors ship good-enough sandboxing for free.** Claude
|
|
||||||
Code now has Seatbelt/bubblewrap + a network proxy natively; Codex
|
|
||||||
has its own sandbox + approvals. This compresses the *single-vendor,
|
|
||||||
single-dev* market to ~zero willingness-to-pay. It is *why* the
|
|
||||||
product must be cross-vendor fleet governance, not local agent
|
|
||||||
safety.
|
|
||||||
2. **Northflank is converging from the infra side.** It already ships
|
|
||||||
dedicated egress gateways + proxy-based secret injection + BYOC.
|
|
||||||
It is the nearest thing to bot-bottle's differentiator as a managed
|
|
||||||
platform — but infra-first and agent-neutral, not agent-aware,
|
|
||||||
cross-vendor, or audit-first. Watch it.
|
|
||||||
|
|
||||||
## Monetization path (sequenced)
|
|
||||||
|
|
||||||
Open-core: **give away the sandbox, charge for the control plane.**
|
|
||||||
|
|
||||||
- **Phase 0 — validate (1–2 wks, parallel).** Ask 5–10 teams running 2+
|
|
||||||
agents: would you pay for one egress-audit + policy plane across
|
|
||||||
Claude *and* Codex? Gate the rest on a yes.
|
|
||||||
- **Phase 1 — the wedge (self-hostable, OSS).** Multi-bottle egress
|
|
||||||
dashboard + web approval queue + exportable audit log, built over the
|
|
||||||
existing `supervise_server.py` JSON-RPC and the egress event levels
|
|
||||||
(`LOG_BLOCKS` / `LOG_FULL`). Low risk, half-built, and the 30-second
|
|
||||||
demo that sells everything. The compliance hook (75% of enterprises
|
|
||||||
rank auditability #1) lives here.
|
|
||||||
- **Phase 2 — the paywall (hosted team tier).** Multi-tenant supervisor:
|
|
||||||
SSO/RBAC, audit retention, alerting, **centralized policy push**
|
|
||||||
(define egress allowlist + DLP once, enforce across all agents —
|
|
||||||
the moat made concrete). Gate on team/compliance features, *never* on
|
|
||||||
the core security.
|
|
||||||
- **Phase 3 — Fly remote backend.** Managed agent pool → "fleet" becomes
|
|
||||||
literal; metered (agent-hours) billing; subsumes remote spin-up +
|
|
||||||
tunnel.
|
|
||||||
- **Phase 4 — deepen.** Second agent provider done deeply (lean
|
|
||||||
open-source/open-weight for rug-pull resistance); egress anomaly
|
|
||||||
detection (the DLP stream becomes a product); SOC2/audit-export for
|
|
||||||
larger buyers.
|
|
||||||
|
|
||||||
**Do not build first:** the p2p mobile app (least monetizable, 6PN
|
|
||||||
gives the tunnel free), a generic multi-cloud abstraction (premature),
|
|
||||||
or the hosted SaaS before Phase 0.
|
|
||||||
|
|
||||||
## Brand vs. revenue: the solo-dev / Linux instinct
|
|
||||||
|
|
||||||
The instinct to court Linux/hacker/solo-dev users and stay "not too
|
|
||||||
corporate" is **right for distribution, dangerous as strategy.**
|
|
||||||
|
|
||||||
- **Right:** it's how OSS infra gets discovered and trusted (HN, stars,
|
|
||||||
word-of-mouth, security-circle vouching); authenticity is a real moat
|
|
||||||
vs. the corporate players *because the architecture sincerely embodies
|
|
||||||
it* (local-first, `$HOME` trust boundary, no phone-home); and it fits
|
|
||||||
the founder.
|
|
||||||
- **Dangerous:** that audience is the lowest-WTP cohort that exists
|
|
||||||
(self-hosts the free thing, forks rather than pays), and "not too
|
|
||||||
corporate" reads to a VP of Eng as "not enterprise-ready." Building an
|
|
||||||
anti-SaaS brand and then shipping a paid tier invites the sell-out /
|
|
||||||
rug-pull backlash — which **Daytona just triggered** going closed.
|
|
||||||
|
|
||||||
**Resolution — be Tailscale, not a manifesto.** Use the developer-first,
|
|
||||||
respects-you energy as the *funnel*; sell *through* the solo advocate,
|
|
||||||
bottom-up, into the team that pays. Two guardrails:
|
|
||||||
|
|
||||||
1. "Anti-corporate" must not mean "anti-team-features." SSO/RBAC/audit
|
|
||||||
retention *are* the monetization; build them in a developer-respecting
|
|
||||||
way (Tailscale has SSO and is still beloved). Tone is the brand; team
|
|
||||||
features are the product.
|
|
||||||
2. Set the open-core social contract publicly **on day one** — core
|
|
||||||
sandbox open and self-hostable forever; hosted control plane is how
|
|
||||||
the lights stay on. The communities that don't revolt are the ones
|
|
||||||
told the deal upfront.
|
|
||||||
|
|
||||||
Concrete: the README frames the Docker/**Linux** backend as "legacy."
|
|
||||||
If courting the Linux crowd, make the Linux path (Docker+gVisor,
|
|
||||||
libkrun/smolmachines) first-class in the docs, not the fallback.
|
|
||||||
|
|
||||||
## Individuals, mobile, and the Pi-ecosystem reality check
|
|
||||||
|
|
||||||
"Individual devs won't pay" (above) is too blunt and needs refining.
|
|
||||||
The accurate claim: individuals won't pay for **safety-as-insurance**
|
|
||||||
(abstract risk reduction the labs give away free), but they *do* pay for
|
|
||||||
**capability/convenience felt daily** — Claude Pro, Cursor, Tailscale
|
|
||||||
Personal. "Drive my self-hosted agent from my phone" is capability, not
|
|
||||||
insurance, so it has a real (low-priced, high-churn) WTP profile. The
|
|
||||||
self-hoster/Linux crowd specifically pays for **sovereignty/control**,
|
|
||||||
just not for enterprise insurance. So an individual "sovereign remote
|
|
||||||
agent access" tier is *not* unreasonable in principle.
|
|
||||||
|
|
||||||
**But the market has already run that experiment, in public, for free.**
|
|
||||||
The Pi ecosystem (pi.dev) has commoditized every convenience layer an
|
|
||||||
individual product would charge for:
|
|
||||||
|
|
||||||
| Capability | Already free/OSS | bot-bottle differentiates? |
|
|
||||||
| :-- | :-- | :-- |
|
|
||||||
| Remote control from mobile | remote-pi, Paseo, TelePi | ❌ commoditized |
|
|
||||||
| Multi-agent orchestration from mobile | Paseo, pi-agent-dashboard | ❌ commoditized |
|
|
||||||
| **Launch** new agents from mobile | Paseo (`paseo run`) | ❌ commoditized |
|
|
||||||
| Launch into a **sandboxed, egress-audited** env | nobody | ✅ the moat |
|
|
||||||
|
|
||||||
Paseo (`getpaseo/paseo`, on the App Store) does the full thing an
|
|
||||||
individual remote-control tier would charge for — launch *and* attach
|
|
||||||
agents on a laptop/VM/dev-server, driven from mobile over an E2E relay —
|
|
||||||
free and open source. It *orchestrates* agents; it does **not** sandbox them, run
|
|
||||||
an egress chokepoint, DLP-scan, or audit. None of the Pi-ecosystem tools
|
|
||||||
do. So the residue, yet again, is **isolation + governance**, not
|
|
||||||
remote/launch convenience.
|
|
||||||
|
|
||||||
Two takeaways:
|
|
||||||
1. **Don't compete on orchestration/launch/remote UX** — it's a solved,
|
|
||||||
free, fast-moving, App-Store-shipping space around Pi. You won't win
|
|
||||||
it and it isn't the moat.
|
|
||||||
2. **Be the safe runtime orchestrators launch *into*.** Launch-from-mobile
|
|
||||||
is table stakes; *launch-into-a-sealed-egress-audited-bottle* is the
|
|
||||||
differentiator. bot-bottle is the sandbox an orchestrator like Paseo
|
|
||||||
would target, or that you wrap thin orchestration around — never the
|
|
||||||
orchestrator itself.
|
|
||||||
|
|
||||||
Capability layers commoditize fast: every individual/mobile angle
|
|
||||||
probed in this analysis collapsed back to the same cross-vendor +
|
|
||||||
sandbox + egress-audit + custody bundle. Mobile remote belongs as a
|
|
||||||
*funnel delighter* on top of the team product, not a standalone paid
|
|
||||||
line.
|
|
||||||
|
|
||||||
## Forge-native orchestration as the delivery vehicle
|
|
||||||
|
|
||||||
The strongest concrete *product shape* for the moat is not a bespoke
|
|
||||||
dashboard and not a Paseo competitor — it is **the git forge as the
|
|
||||||
orchestrator, with bot-bottle as the safe runtime it launches into.**
|
|
||||||
The forge already provides, for free, everything an orchestrator would
|
|
||||||
otherwise have to build: identity (agent/bot users, signed commits),
|
|
||||||
state (issues, labels, PRs/MRs, comments), triggers (webhooks, CI,
|
|
||||||
comment commands), review (diffs, approvals, status checks), audit
|
|
||||||
(commits/comments/reviews), and permissions (repo access, protected
|
|
||||||
branches, token scopes). bot-bottle supplies the one thing the forge
|
|
||||||
doesn't: **least-privilege, secret-isolated, audited execution of
|
|
||||||
untrusted agents.** Same moat (custody + audit + policy), better
|
|
||||||
vehicle — and it lands the product where teams already live, so it
|
|
||||||
avoids building an agent dashboard before one is needed.
|
|
||||||
|
|
||||||
The flow is essentially free to assemble:
|
|
||||||
|
|
||||||
```
|
|
||||||
issue/PR/MR event → webhook → policy/router → assign agent user +
|
|
||||||
branch/worktree → run agent in an isolated bottle (no ambient secrets)
|
|
||||||
→ commit as agent identity → open PR/MR → CI + human review + merge
|
|
||||||
```
|
|
||||||
|
|
||||||
**Crowding (why this is less saturated than it looks):**
|
|
||||||
|
|
||||||
| Layer | How crowded |
|
|
||||||
| :-- | :-- |
|
|
||||||
| Generic multi-agent orchestrators (worktree/TUI/dashboard) | very — 50–100+ |
|
|
||||||
| Forge-native issue/PR/MR orchestration | moderate — ~10–30 serious |
|
|
||||||
| Self-hostable, least-privilege, audited, forge-portable | **single digits** |
|
|
||||||
|
|
||||||
The deeper you go toward *untrusted-agent safety + auditability +
|
|
||||||
self-hostable + forge-portable*, the emptier it gets.
|
|
||||||
|
|
||||||
**The GitHub/GitLab first-party trap → lead Gitea + sovereignty.**
|
|
||||||
GitHub (Agentic Workflows, Copilot coding agent) and GitLab (Duo Agent
|
|
||||||
Platform) are the forge *vendors* building native issue-to-PR agent
|
|
||||||
orchestration with native identity/permissions/audit. On their turf you
|
|
||||||
lose the integration-depth battle the same way single-vendor agent
|
|
||||||
safety loses to Anthropic/OpenAI — the same "incumbent ships it free,
|
|
||||||
deeper" dynamic, one layer up. So the durable opening is **Gitea +
|
|
||||||
self-hosted** (no first-party agent platform exists — the open Gitea
|
|
||||||
feature request for an AI code agent confirms the vacuum) plus
|
|
||||||
**cross-forge *untrusted-agent* safety**, which no forge vendor will
|
|
||||||
build because they want you running *their* agent, not arbitrary ones
|
|
||||||
under uniform least-privilege across competitors' forges. Cross-vendor
|
|
||||||
neutrality, applied to forges.
|
|
||||||
|
|
||||||
**Buyer reconciliation.** The least-crowded opening (self-hosted Gitea)
|
|
||||||
overlaps the lowest-WTP crowd (indie self-hosters), while the paying
|
|
||||||
teams sit on GitHub/GitLab where first-party competition is fiercest.
|
|
||||||
The intersection that resolves it: **orgs running self-hosted forges for
|
|
||||||
sovereignty/compliance reasons** (regulated, air-gapped, security-
|
|
||||||
conscious, on-prem). They have budget, they run self-hosted GitLab/Gitea,
|
|
||||||
*and* shipping code to a cloud agent vendor is a non-starter — so "run
|
|
||||||
untrusted agents sandboxed, least-privilege, fully audited, inside our
|
|
||||||
forge, on our infra" is a procurement checkbox, not a nicety. That is
|
|
||||||
where "least-crowded" finally meets "has money."
|
|
||||||
|
|
||||||
**Separate moat-hard-parts from cost-hard-parts.** The orchestration
|
|
||||||
"hard parts" are two different things, and conflating them oversells the
|
|
||||||
fit:
|
|
||||||
|
|
||||||
| Moat (your differentiated strength) | Undifferentiated cost (everyone faces) |
|
|
||||||
| :-- | :-- |
|
|
||||||
| permission isolation | idempotency / dedupe / run ledger |
|
|
||||||
| secret handling under malicious prompts | concurrency, locks, cancellation |
|
|
||||||
| run provenance | queueing / scheduling / cleanup |
|
|
||||||
| policy language | merge-conflict handling (~27% agent-PR conflict rate) |
|
|
||||||
|
|
||||||
The right column is generic distributed-systems plumbing that wins you
|
|
||||||
nothing and that merge-conflict resolution especially is a *different
|
|
||||||
competency* from sandbox/custody. Keep it thin in the MVP; do not build a
|
|
||||||
policy DSL + durable ledger + conflict resolver before one org pays.
|
|
||||||
|
|
||||||
**The killer feature: run provenance on every agent PR.** A check/comment
|
|
||||||
answering — which agent, which model, which prompt, which base commit,
|
|
||||||
which policy, which tools, which network egress, which test results —
|
|
||||||
attached at the moment a human reviews. It renders the (invisible)
|
|
||||||
custody + egress-audit work as a PR artifact the buyer sees at the exact
|
|
||||||
trust-decision point. No forge vendor's first-party agent will show you
|
|
||||||
"here is everything the untrusted agent could reach." Build this first.
|
|
||||||
|
|
||||||
**MVP** (`@bot-bottle fix this`): create an isolated worktree/bottle →
|
|
||||||
check out the issue branch → run the selected harness as a named agent
|
|
||||||
user → deny ambient secrets by default → record prompt/model/tools/policy
|
|
||||||
→ commit with bot identity → open PR/MR → attach the run-provenance
|
|
||||||
footer (log + tests + permission/egress summary) → require human merge.
|
|
||||||
The security model *is* the product. This rides the headless launch
|
|
||||||
primitive directly: webhook → `start --headless` into an isolated bottle
|
|
||||||
→ commit as agent identity → PR with provenance.
|
|
||||||
|
|
||||||
Open-core line is unchanged: the webhook/comment trigger stays free
|
|
||||||
(adoption); the sandboxed-execution + provenance + policy layer is the
|
|
||||||
paid governance.
|
|
||||||
|
|
||||||
## Risks to the thesis
|
|
||||||
|
|
||||||
- **Lab encroachment.** If Anthropic/OpenAI add cross-agent governance
|
|
||||||
or open their managed egress logs, the wedge narrows. Mitigate by
|
|
||||||
going deep on cross-vendor + custody + audit *now*, while they're
|
|
||||||
single-vendor.
|
|
||||||
- **Rug-pull dependency.** You run the labs' agents; they can restrict
|
|
||||||
their agent to their own sandbox via ToS/tech. Hedge toward
|
|
||||||
open-source/open-weight agents for durability.
|
|
||||||
- **Northflank (or E2B) ships agent-aware audit.** Plausible from the
|
|
||||||
infra side. Your defense is agent-awareness + the supervise approval
|
|
||||||
loop + cross-vendor, not raw egress visibility.
|
|
||||||
- **WTP may simply not be there.** The honest failure mode: teams like
|
|
||||||
the audit but won't pay because "we already sandbox in CI." Phase 0
|
|
||||||
exists to find this out cheaply before building Phase 2/3.
|
|
||||||
- **Forge-vendor encroachment (forge-native path).** GitHub Agentic
|
|
||||||
Workflows / Copilot and GitLab Duo are first-party and deepening.
|
|
||||||
Defense: aim at self-hosted Gitea + sovereignty buyers where no
|
|
||||||
first-party agent platform exists, and at cross-forge untrusted-agent
|
|
||||||
neutrality the vendors won't build. Don't fight them GitHub-native.
|
|
||||||
- **Orchestration-reliability scope creep.** The forge-native build
|
|
||||||
drags in idempotency, queueing, concurrency, and merge-conflict
|
|
||||||
handling — undifferentiated plumbing that isn't the moat. Keep it thin
|
|
||||||
until a paying org forces it.
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
Build Phase 1 now — it's low-risk, half-built, and the proof artifact.
|
|
||||||
Run Phase 0 in parallel. Treat a clear yes from 5–10 teams as the
|
|
||||||
green light for the hosted tier; treat a soft maybe as a signal to stay
|
|
||||||
an excellent OSS tool with a tip-jar/support model rather than a
|
|
||||||
venture-shaped SaaS. The technology is not the risk — the codebase is
|
|
||||||
exemplary and the architecture already supports the pivot. The risk is
|
|
||||||
**positioning discipline**: sell cross-vendor fleet governance to teams,
|
|
||||||
use the indie brand as the funnel, and never let the anti-corporate
|
|
||||||
aesthetic veto the features that pay.
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
- Anthropic — Claude Code sandboxing:
|
|
||||||
https://www.anthropic.com/engineering/claude-code-sandboxing
|
|
||||||
- OpenAI Codex — cloud environments:
|
|
||||||
https://developers.openai.com/codex/cloud/environments ;
|
|
||||||
custom-image feature request:
|
|
||||||
https://community.openai.com/t/feature-request-custom-docker-images/1265333
|
|
||||||
- GitHub Copilot — custom container image (not supported), discussion
|
|
||||||
#194105: https://github.com/orgs/community/discussions/194105
|
|
||||||
- DeepInspect — AI egress monitoring:
|
|
||||||
https://www.deepinspect.ai/blog/ai-egress-monitoring
|
|
||||||
- Braintrust — AI agent observability/alerting:
|
|
||||||
https://www.braintrust.dev/articles/best-ai-agent-observability-tools-2026
|
|
||||||
- E2B (OSS, Apache-2.0): https://github.com/e2b-dev/e2b ;
|
|
||||||
infra/self-host: https://github.com/e2b-dev/infra
|
|
||||||
- Daytona going closed source:
|
|
||||||
https://www.daytona.io/dotfiles/updates/daytona-is-going-closed-source
|
|
||||||
- Northflank — BYOC / egress gateways:
|
|
||||||
https://northflank.com/blog/what-is-byoc-in-cloud-computing ;
|
|
||||||
https://northflank.com/blog/self-hostable-alternatives-to-e2b-for-ai-agents
|
|
||||||
- Modal Sandboxes: https://modal.com/products/sandboxes
|
|
||||||
- AI agent orchestration / enterprise governance (75% cite
|
|
||||||
auditability):
|
|
||||||
https://viston.tech/ai-agent-orchestration-in-2026-moving-from-pilots-to-enterprise-wide-execution/
|
|
||||||
- Pi harness (provider-agnostic CLI): https://pi.dev/packages/remote-pi ;
|
|
||||||
https://github.com/earendil-works/pi
|
|
||||||
- Paseo (launch + attach agents from desktop/mobile, OSS):
|
|
||||||
https://github.com/getpaseo/paseo ;
|
|
||||||
https://apps.apple.com/us/app/paseo-remote-coding-agents/id6758887924
|
|
||||||
- pi-agent-dashboard (mobile-first remote control via mDNS/zrok):
|
|
||||||
https://github.com/BlackBeltTechnology/pi-agent-dashboard
|
|
||||||
- TelePi (Telegram remote control for Pi):
|
|
||||||
https://futurelab.studio/blog/telepi-telegram-remote-control-for-pi/
|
|
||||||
- Forge-native landscape (provided via conversation, not independently
|
|
||||||
re-verified):
|
|
||||||
- awesome-agent-orchestrators (50+ generic orchestrators):
|
|
||||||
https://github.com/andyrewlee/awesome-agent-orchestrators
|
|
||||||
- GitHub Agentic Workflows (first-party repo automation):
|
|
||||||
https://github.blog/ai-and-ml/automate-repository-tasks-with-github-agentic-workflows/
|
|
||||||
- GitLab Duo Agent Platform GA:
|
|
||||||
https://ir.gitlab.com/news/news-details/2026/GitLab-Announces-the-General-Availability-of-GitLab-Duo-Agent-Platform/default.aspx
|
|
||||||
- ai-review (cross-forge review incl. Gitea):
|
|
||||||
https://github.com/Nikita-Filonov/ai-review
|
|
||||||
- Gitea feature request — AI code agent (the vacuum):
|
|
||||||
https://github.com/go-gitea/gitea/issues/34527
|
|
||||||
- Phoenix — safe GitHub issue resolution (label-based webhook state
|
|
||||||
machine): https://arxiv.org/abs/2606.20243
|
|
||||||
- AgenticFlict — ~27% merge-conflict rate in agent PRs:
|
|
||||||
https://arxiv.org/abs/2604.03551
|
|
||||||
@@ -165,22 +165,6 @@ class TestAgentValidation(unittest.TestCase):
|
|||||||
with self.assertRaises(ManifestError):
|
with self.assertRaises(ManifestError):
|
||||||
ManifestAgent.from_dict("a", {"skills": [5]}, set())
|
ManifestAgent.from_dict("a", {"skills": [5]}, set())
|
||||||
|
|
||||||
def test_skill_name_rejects_shell_metacharacters(self) -> None:
|
|
||||||
# Skill names become host/guest path segments interpolated into
|
|
||||||
# provisioning shell commands; anything outside kebab-case is
|
|
||||||
# rejected at load so it can never reach a `bottle.exec` string.
|
|
||||||
for bad in ("foo; rm -rf /", "../escape", "foo bar", "Foo", "-leading"):
|
|
||||||
with self.assertRaises(ManifestError):
|
|
||||||
ManifestAgent.from_dict("a", {"skills": [bad]}, set())
|
|
||||||
|
|
||||||
def test_skill_name_accepts_kebab_case(self) -> None:
|
|
||||||
agent = ManifestAgent.from_dict(
|
|
||||||
"a", {"skills": ["init-entry", "quality-eval", "skill0"]}, set()
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
agent.skills, ("init-entry", "quality-eval", "skill0")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_prompt_not_string(self) -> None:
|
def test_prompt_not_string(self) -> None:
|
||||||
with self.assertRaises(ManifestError):
|
with self.assertRaises(ManifestError):
|
||||||
ManifestAgent.from_dict("a", {"prompt": 5}, set())
|
ManifestAgent.from_dict("a", {"prompt": 5}, set())
|
||||||
|
|||||||
Reference in New Issue
Block a user