agent-container 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. agent_container-0.1.0/.env.example +25 -0
  2. agent_container-0.1.0/.github/workflows/ci.yml +33 -0
  3. agent_container-0.1.0/.github/workflows/publish.yml +54 -0
  4. agent_container-0.1.0/.gitignore +29 -0
  5. agent_container-0.1.0/CLAUDE.md +47 -0
  6. agent_container-0.1.0/Dockerfile +166 -0
  7. agent_container-0.1.0/LICENSE +21 -0
  8. agent_container-0.1.0/PKG-INFO +753 -0
  9. agent_container-0.1.0/README.md +727 -0
  10. agent_container-0.1.0/bin/agent-container +1327 -0
  11. agent_container-0.1.0/bin/tests/conftest.py +92 -0
  12. agent_container-0.1.0/bin/tests/test_cli.py +198 -0
  13. agent_container-0.1.0/bin/tests/test_command_construction.py +444 -0
  14. agent_container-0.1.0/bin/tests/test_completions.sh +242 -0
  15. agent_container-0.1.0/bin/tests/test_entrypoint.sh +164 -0
  16. agent_container-0.1.0/bin/tests/test_entrypoint_tmux_layout.sh +127 -0
  17. agent_container-0.1.0/bin/tests/test_packaging.py +187 -0
  18. agent_container-0.1.0/bin/tests/test_pure_logic.py +329 -0
  19. agent_container-0.1.0/completions/agent-container.bash +172 -0
  20. agent_container-0.1.0/completions/agent-container.zsh +137 -0
  21. agent_container-0.1.0/completions/oh-my-zsh/agent-container/agent-container.plugin.zsh +49 -0
  22. agent_container-0.1.0/docs/agent-container-hosts.example +23 -0
  23. agent_container-0.1.0/docs/credentials.md +114 -0
  24. agent_container-0.1.0/docs/decisions/0001-runtime-and-base-image.md +84 -0
  25. agent_container-0.1.0/docs/orchestration.md +142 -0
  26. agent_container-0.1.0/docs/smoke-test.md +51 -0
  27. agent_container-0.1.0/entrypoint.sh +189 -0
  28. agent_container-0.1.0/orchestration/agent-container.container +37 -0
  29. agent_container-0.1.0/orchestration/compose.yaml +34 -0
  30. agent_container-0.1.0/pyproject.toml +72 -0
  31. agent_container-0.1.0/scripts/smoke-test.sh +162 -0
  32. agent_container-0.1.0/uv.lock +138 -0
@@ -0,0 +1,25 @@
1
+ # Copy to .env and fill in real values. .env is gitignored; never commit it.
2
+ # See docs/credentials.md for the rationale and security trade-offs.
3
+
4
+ # --- GitHub auth (HTTPS push) ---
5
+ # Create at: https://github.com/settings/tokens
6
+ # Scopes: repo (required), workflow (optional)
7
+ GH_TOKEN=
8
+
9
+ # --- Git identity (not secret) ---
10
+ GIT_USER_NAME=Your Name
11
+ GIT_USER_EMAIL=you@example.com
12
+
13
+ # --- Agent provider keys ---
14
+ # OPTIONAL: leave blank to log in interactively inside the container instead
15
+ # (claude -> /login, codex login). Interactive login persists on the container's
16
+ # per-name credential volume and auto-refreshes — preferred for subscription
17
+ # accounts. See docs/credentials.md and the README "Log in to agents" section.
18
+ # Claude Code (API-key billing):
19
+ ANTHROPIC_API_KEY=
20
+ # Codex (@openai/codex):
21
+ OPENAI_API_KEY=
22
+ # pi-coding-agent is multi-provider; uncomment the ones you actually use.
23
+ # GOOGLE_API_KEY=
24
+ # MISTRAL_API_KEY=
25
+ # GROQ_API_KEY=
@@ -0,0 +1,33 @@
1
+ # CI gate for agent-container: runs the runtime-free pytest suite and builds the
2
+ # distributions on every push and PR. This does NOT run on tag pushes; the
3
+ # release itself is gated by publish.yml, which re-runs the suite before build.
4
+ name: ci
5
+
6
+ on:
7
+ push:
8
+ branches: ["**"]
9
+ pull_request:
10
+
11
+ # Cancel superseded runs for the same ref so rapid successive pushes don't pile
12
+ # up runners (does not dedupe the push/pull_request pair — different refs).
13
+ concurrency:
14
+ group: ci-${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ test:
19
+ name: pytest + build
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v5
25
+ - name: Run the pytest suite
26
+ # --no-project keeps the run hermetic; the --with pins mirror the
27
+ # bin/agent-container PEP 723 inline metadata (kept in sync with pyproject).
28
+ run: >
29
+ uv run --no-project --with pytest
30
+ --with 'typer>=0.12,<1' --with 'questionary>=2.0,<3' --with 'rich>=13,<15'
31
+ pytest bin/tests
32
+ - name: Build distributions
33
+ run: uv build
@@ -0,0 +1,54 @@
1
+ # Publish agent-container to PyPI via Trusted Publishing (OIDC — no stored secrets).
2
+ #
3
+ # Trigger: pushing a tag matching v* (e.g. v0.1.0). Builds an sdist + wheel with
4
+ # `uv build` and uploads them with pypa/gh-action-pypi-publish, which mints a
5
+ # short-lived OIDC token. The operator configures the PyPI trusted publisher once
6
+ # (project: agent-container, owner: ondrasek, repo: agent-container, workflow: publish.yml,
7
+ # environment: release) — no API token is ever stored in the repo.
8
+ name: publish
9
+
10
+ on:
11
+ push:
12
+ tags:
13
+ - "v*"
14
+
15
+ jobs:
16
+ build:
17
+ name: Build sdist + wheel
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v5
23
+ - name: Run the pytest suite
24
+ # Gate the release on green: mirrors ci.yml (ci.yml does NOT run on tag
25
+ # pushes, so the suite is re-run here). A failure here fails the build
26
+ # job, and `publish` needs: build — so a red tagged commit never ships.
27
+ # --with pins mirror the bin/agent-container PEP 723 inline metadata.
28
+ run: >
29
+ uv run --no-project --with pytest
30
+ --with 'typer>=0.12,<1' --with 'questionary>=2.0,<3' --with 'rich>=13,<15'
31
+ pytest bin/tests
32
+ - name: Build distributions
33
+ run: uv build
34
+ - name: Upload dist artifact
35
+ uses: actions/upload-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+
40
+ publish:
41
+ name: Publish to PyPI
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment: release
45
+ permissions:
46
+ id-token: write # OIDC token for Trusted Publishing
47
+ steps:
48
+ - name: Download dist artifact
49
+ uses: actions/download-artifact@v4
50
+ with:
51
+ name: dist
52
+ path: dist/
53
+ - name: Publish to PyPI
54
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ # Runtime credentials and per-host config — see docs/credentials.md
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+
6
+ # Claude Code harness artifacts (workflow scripts saved per-session)
7
+ .claude/
8
+
9
+ # OS noise
10
+ .DS_Store
11
+ Thumbs.db
12
+
13
+ # Editor noise
14
+ *.swp
15
+ *.swo
16
+ .idea/
17
+
18
+ # Python build artifacts (bin/tests pytest suite)
19
+ __pycache__/
20
+ *.pyc
21
+ .pytest_cache/
22
+
23
+ # Packaging / uv project mode (root pyproject.toml)
24
+ .venv/
25
+ *.egg-info/
26
+ dist/
27
+ build/
28
+ # NOTE: uv.lock IS committed. agent-container is an application/CLI (not a library), so
29
+ # the lockfile is checked in for reproducible dev envs (`uv sync`). Do not ignore it.
@@ -0,0 +1,47 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project intent
6
+
7
+ Build a **containerized development environment** designed to run remotely (e.g., Hetzner VPS) as an always-on container that the user attaches to and detaches from over SSH. Inside, multiple AI coding agents (Claude Code, Codex, pi-coding-agent) and editors (nvim) run under tmux. Multiple such containers may run in parallel, each holding working copies of one or more git repositories.
8
+
9
+ This is a greenfield repo — no code has been written yet. Treat the section below as the design contract, not a description of existing code.
10
+
11
+ ## Hard constraints
12
+
13
+ These are load-bearing design decisions, not preferences:
14
+
15
+ 1. **No reliance on container persistence.** Every agent must `commit` AND `push` every change. The container is treated as ephemeral; if it dies, no work is lost. Any feature or workflow that depends on uncommitted state is wrong by construction.
16
+ 2. **Editor-agnostic, not VSCode-locked.** The user explicitly rejected devcontainers because tooling support outside VSCode is nonexistent. Do not introduce `.devcontainer/` configs or any design that assumes a VSCode client. SSH + tmux is the canonical attach path.
17
+ 3. **Multiple parallel containers.** Naming, port allocation, volume mounts, and git identity must all support N containers running simultaneously on the same host without collision.
18
+ 4. **Push auth must work non-interactively.** Agents commit autonomously, so SSH keys / git credentials inside the container must be configured to push without prompts. Never embed long-lived secrets in the image — inject at runtime.
19
+
20
+ ## Decisions
21
+
22
+ - **Runtime + base image:** Podman + `debian:12-slim`. See [`docs/decisions/0001-runtime-and-base-image.md`](docs/decisions/0001-runtime-and-base-image.md).
23
+ - **CLI:** a single tool, `agent-container` (`bin/agent-container`) — a PEP 723 uv script (Typer + questionary + rich) covering the whole lifecycle (build/up/attach/logs/down/purge) plus an interactive wizard. Its on-disk contract (container names `agent-container-<name>`, the `2200 + name-hash` port, `$XDG_STATE_HOME/agent-container/<name>.port` state files, `~/.config/agent-container/hosts.conf`) is the single source of truth; the shell completions read the same state files. Runtime default is platform-aware (docker-first on macOS, podman-first on Linux); override with `AGENT_CONTAINER_RUNTIME`.
24
+ - **Packaging + release:** ships to PyPI as the `agent_container` module via a hatchling `force-include` wheel (`bin/agent-container` → `agent_container/__init__.py`; completions → `agent_container/completions/*` package data). `REPO_ROOT` resolves location-independently (`AGENT_CONTAINER_REPO` → `Dockerfile`+`completions/` marker → `None`) so a **non-editable** PyPI install works standalone (`up/down/list/attach/logs/purge/completions`); only `build` needs a checkout (via `AGENT_CONTAINER_REPO`/`--context`). `uv tool install --editable .` remains the dev path. Push a `v*` tag → `.github/workflows/publish.yml` uploads via PyPI **Trusted Publishing** (OIDC, no stored secrets); `ci.yml` gates on the pytest suite + `uv build`. Licensed **MIT** ([`LICENSE`](LICENSE)).
25
+
26
+ ## Architecture sketch (to be built)
27
+
28
+ Expect the repo to grow into roughly:
29
+
30
+ - **Container image** — base OS + tmux + SSH server + nvim + git + the agent CLIs (Claude Code, Codex, pi-coding-agent) and their language runtimes.
31
+ - **Orchestration layer** — scripts or compose/quadlet definitions to launch, name, attach to, and tear down containers on the remote host.
32
+ - **Bootstrap / entrypoint** — sets up git identity, injects credentials, starts sshd + a default tmux session, optionally clones configured repos.
33
+ - **Attach tooling** — a thin client-side helper for `ssh user@host -t tmux attach -t <session>` style flows across multiple hosts/containers.
34
+
35
+ When adding a component, keep these layers separate. Don't bake host-specific orchestration into the image.
36
+
37
+ ## Conventions for future work
38
+
39
+ - Prefer **rootless / Podman-compatible** patterns where reasonable; avoid features that only work on Docker Desktop.
40
+ - Treat the **commit-and-push discipline** as a property of the agent configuration, not something to enforce via git hooks alone (hooks can be bypassed; the agents themselves should be configured to push).
41
+ - When proposing a tool or dependency, justify it against the constraints above — especially the "not VSCode-locked" one.
42
+
43
+ ## Out of scope (don't add unless asked)
44
+
45
+ - IDE integrations beyond plain SSH/tmux/nvim.
46
+ - Multi-user / multi-tenant access controls — single operator (the user) is assumed.
47
+ - Kubernetes manifests — the target is a single VPS running a container runtime, not a cluster.
@@ -0,0 +1,166 @@
1
+ # syntax-compatible with both `docker build` and `podman build`.
2
+ # No BuildKit-specific features. See docs/decisions/0001-runtime-and-base-image.md.
3
+ #
4
+ # Layering rationale (cheapest-to-rebuild last):
5
+ # 1. apt base packages — rarely changes; large; cached aggressively.
6
+ # 2. Node 22 LTS (NodeSource) — rarely changes; medium.
7
+ # 3. Agent CLIs (npm globals) — changes with agent releases.
8
+ # 4. Neovim upstream tarball — changes with nvim releases.
9
+ # 5. User + sshd config — cheap; placed late so earlier layers cache.
10
+ # 6. entrypoint.sh — changes most often (item C); last.
11
+
12
+ FROM debian:12-slim
13
+
14
+ # Fail fast on any unhandled error inside RUN blocks.
15
+ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
16
+
17
+ ENV DEBIAN_FRONTEND=noninteractive \
18
+ LANG=C.UTF-8 \
19
+ LC_ALL=C.UTF-8 \
20
+ TZ=UTC
21
+
22
+ # --- Layer 1: base OS packages ----------------------------------------------
23
+ # Single RUN so the apt cache cleanup lands in the same layer as the install.
24
+ RUN apt-get update \
25
+ && apt-get install -y --no-install-recommends \
26
+ ca-certificates \
27
+ curl \
28
+ gnupg \
29
+ git \
30
+ openssh-server \
31
+ tmux \
32
+ zsh \
33
+ sudo \
34
+ locales \
35
+ less \
36
+ jq \
37
+ build-essential \
38
+ python3 \
39
+ python3-pip \
40
+ python3-venv \
41
+ && sed -i 's/^# *\(C.UTF-8\)/\1/' /etc/locale.gen || true \
42
+ && locale-gen C.UTF-8 \
43
+ && apt-get clean \
44
+ && rm -rf /var/lib/apt/lists/*
45
+
46
+ # --- Layer 2: Node 22 LTS via NodeSource ------------------------------------
47
+ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
48
+ && apt-get install -y --no-install-recommends nodejs \
49
+ && apt-get clean \
50
+ && rm -rf /var/lib/apt/lists/*
51
+
52
+ # --- Layer 3: agent CLIs (global npm installs) ------------------------------
53
+ # pi-coding-agent needs --ignore-scripts (postinstall pulls native bits we
54
+ # don't want resolved at build time).
55
+ RUN npm i -g @anthropic-ai/claude-code \
56
+ && npm i -g @openai/codex \
57
+ && npm i -g --ignore-scripts @earendil-works/pi-coding-agent \
58
+ && npm cache clean --force
59
+
60
+ # --- Layer 4: Neovim from upstream stable tarball ---------------------------
61
+ # Debian 12's nvim package is too old. Upstream ships per-architecture tarballs;
62
+ # pick the one matching the build platform (amd64 -> x86_64 / linux64 legacy
63
+ # name; arm64 -> linux-arm64). The asset name for amd64 also changed across
64
+ # releases (nvim-linux64.tar.gz -> nvim-linux-x86_64.tar.gz); fall through.
65
+ RUN set -eux; \
66
+ cd /tmp; \
67
+ BASE="https://github.com/neovim/neovim/releases/latest/download"; \
68
+ ARCH="$(dpkg --print-architecture)"; \
69
+ case "${ARCH}" in \
70
+ amd64) CANDIDATES="nvim-linux-x86_64.tar.gz nvim-linux64.tar.gz" ;; \
71
+ arm64) CANDIDATES="nvim-linux-arm64.tar.gz" ;; \
72
+ *) echo "unsupported architecture: ${ARCH}" >&2; exit 1 ;; \
73
+ esac; \
74
+ TARBALL=""; \
75
+ for CAND in ${CANDIDATES}; do \
76
+ if curl -fsSLO "${BASE}/${CAND}"; then TARBALL="${CAND}"; break; fi; \
77
+ done; \
78
+ if [ -z "${TARBALL}" ]; then \
79
+ echo "no neovim release asset matched for arch=${ARCH} (tried: ${CANDIDATES})" >&2; \
80
+ exit 1; \
81
+ fi; \
82
+ tar -C /usr/local -xzf "${TARBALL}" --strip-components=1; \
83
+ rm -f "${TARBALL}"; \
84
+ nvim --version | head -n1
85
+
86
+ # --- Layer 5: user + sudo + sshd config -------------------------------------
87
+ RUN useradd --create-home --home-dir /home/dev --shell /bin/bash --uid 1000 dev \
88
+ && echo 'dev ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/90-dev \
89
+ && chmod 0440 /etc/sudoers.d/90-dev \
90
+ && mkdir -p /workspace \
91
+ && chown dev:dev /workspace \
92
+ && chmod 0755 /workspace
93
+
94
+ # sshd: dev-only, key-based, no root, no passwords.
95
+ # Host keys are deliberately NOT generated here — entrypoint does it at first
96
+ # launch so every container instance gets a distinct identity.
97
+ RUN mkdir -p /etc/ssh \
98
+ && rm -f /etc/ssh/ssh_host_* \
99
+ && { \
100
+ echo 'AllowUsers dev'; \
101
+ echo 'PasswordAuthentication no'; \
102
+ echo 'PermitRootLogin no'; \
103
+ echo 'PubkeyAuthentication yes'; \
104
+ echo 'Port 22'; \
105
+ echo 'UsePAM yes'; \
106
+ echo 'ChallengeResponseAuthentication no'; \
107
+ echo 'PrintMotd no'; \
108
+ echo 'AcceptEnv LANG LC_*'; \
109
+ } > /etc/ssh/sshd_config.d/10-agent-container.conf \
110
+ && mkdir -p /home/dev/.ssh \
111
+ && chown dev:dev /home/dev/.ssh \
112
+ && chmod 0700 /home/dev/.ssh
113
+
114
+ # --- Layer 5b: per-container volume mount points + persistent shell env -----
115
+ # Each of these dirs is the mount point of a per-container named volume
116
+ # (agent-container-<name>-{claude,codex,pi,shellenv,tmux}). Pre-creating them dev-owned
117
+ # (uid 1000) BEFORE the USER switch means a fresh, empty named volume
118
+ # initializes dev-owned too — the runtime seeds a new volume from the image
119
+ # directory's contents AND ownership/permissions. Without this the volume
120
+ # would come up root-owned and the agents could not write their credentials.
121
+ #
122
+ # pi-coding-agent's config/auth dir is ~/.pi (verified: package.json piConfig
123
+ # .configDir = ".pi"; dist/config.js getAgentDir() -> ~/.pi/agent, auth.json
124
+ # at ~/.pi/agent/auth.json). Overridable at runtime via PI_CODING_AGENT_DIR.
125
+ # The credential dirs are 0700; .agent-container (shell env) and .config/tmux (tmux.conf
126
+ # + tpm plugins) are non-secret and 0755. ~/.config/tmux is XDG-standard: tmux
127
+ # 3.x on debian:12 reads ~/.config/tmux/tmux.conf, so persisting that dir on its
128
+ # own named volume carries the operator's tmux.conf/plugins across down/up.
129
+ #
130
+ # Shell-env wiring: agents and the operator drop KEY=VALUE exports in
131
+ # ~/.agent-container/env (on the shellenv volume, so it survives down/up). BOTH bash and
132
+ # zsh source it, guarded so a malformed file cannot break the shell. dev's
133
+ # LOGIN SHELL stays bash (sshd/tmux/entrypoint assume bash); zsh is available
134
+ # but not the default.
135
+ #
136
+ # The hook lives in ~/.bashrc (read by interactive shells). Login shells — SSH
137
+ # login sessions AND tmux panes (tmux spawns the default shell as a LOGIN shell
138
+ # when no default-command is set) — read ~/.bash_profile, not ~/.bashrc, so
139
+ # ~/.bash_profile must chain to ~/.bashrc. We source Debian's stock ~/.profile
140
+ # to do that: it sources ~/.bashrc under bash (firing the hook) AND restores the
141
+ # ~/.local/bin + ~/bin PATH additions that ~/.bash_profile would otherwise shadow
142
+ # (e.g. for `pip install --user` tools). This bash_profile->profile->bashrc chain
143
+ # is load-bearing for login-shell panes; do not "simplify" it away.
144
+ RUN set -eux; \
145
+ mkdir -p /home/dev/.claude /home/dev/.codex /home/dev/.pi /home/dev/.agent-container /home/dev/.config/tmux; \
146
+ chown -R dev:dev /home/dev/.claude /home/dev/.codex /home/dev/.pi /home/dev/.agent-container /home/dev/.config; \
147
+ chmod 0700 /home/dev/.claude /home/dev/.codex /home/dev/.pi; \
148
+ chmod 0755 /home/dev/.agent-container /home/dev/.config/tmux; \
149
+ HOOK='if [ -f "$HOME/.agent-container/env" ]; then set -a; . "$HOME/.agent-container/env"; set +a; fi'; \
150
+ printf '\n# agent-container: source persistent shell env if present (guarded)\n%s\n' "$HOOK" >> /home/dev/.bashrc; \
151
+ printf '# agent-container: source persistent shell env if present (guarded)\n%s\n' "$HOOK" >> /home/dev/.zshrc; \
152
+ printf '# agent-container: login shells load ~/.profile (sources ~/.bashrc + adds ~/.local/bin to PATH)\n[ -f "$HOME/.profile" ] && . "$HOME/.profile"\n' >> /home/dev/.bash_profile; \
153
+ chown dev:dev /home/dev/.bashrc /home/dev/.zshrc /home/dev/.bash_profile
154
+
155
+ # --- Layer 6: entrypoint (most-frequently-changed; STUB until item C) -------
156
+ # Two-step COPY+chmod instead of `COPY --chmod=` so the file builds under both
157
+ # the legacy builder and BuildKit; --chmod requires BuildKit specifically.
158
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
159
+ RUN chmod 0755 /usr/local/bin/entrypoint.sh
160
+
161
+ EXPOSE 22
162
+
163
+ USER dev
164
+ WORKDIR /workspace
165
+
166
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ondrej Krajicek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.