mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-21 12:49:07 +00:00
Compare commits
47 Commits
v0.1.0-rc.
...
doc-mcp-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36269d6356 | ||
| 7421500eb0 | |||
| 5b1afa05da | |||
| c869c1117f | |||
| 488715c88b | |||
| 623edeac76 | |||
| 638e2b2a93 | |||
| 5987585b2d | |||
| 1201eb2d3d | |||
| cf31c7c25d | |||
| a7630ace07 | |||
| 150a6fe39c | |||
| c29af828f0 | |||
| cbf2fadc4d | |||
| dd5b9ec213 | |||
| f8b8639bb0 | |||
| 21101320a1 | |||
| 83f6ab9a73 | |||
| 4423f750d5 | |||
| 4b7ae39bba | |||
| dd321d4773 | |||
| 909ba03a7a | |||
| a50336487a | |||
| 0e2e0acada | |||
| b215080052 | |||
| 36dbb83df6 | |||
| 30e34cb544 | |||
| c870eed844 | |||
| 2f84bc1e27 | |||
| 65b0fcfd10 | |||
| 924166d643 | |||
| b120bbaf98 | |||
| a4f1c882c8 | |||
| 429aee2784 | |||
| 0611dc930a | |||
| ac2d310ac7 | |||
| 96a44ef567 | |||
| 2c9d14385a | |||
| 476529c5ef | |||
| 179e4fc939 | |||
| 7e2f807c47 | |||
| 00cee60fe6 | |||
| c56b4b35f5 | |||
| 167d73a964 | |||
| 63734c59ab | |||
| 5218784905 | |||
| dd053dee80 |
17
.github/workflows/conventional_commit_pr.yml
vendored
17
.github/workflows/conventional_commit_pr.yml
vendored
@@ -1,17 +0,0 @@
|
|||||||
name: Conventional commit PR
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cog_check_job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: check conventional commit compliance
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
# pick the pr HEAD instead of the merge commit
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
- name: Conventional commit check
|
|
||||||
uses: cocogitto/cocogitto-action@v3
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name: "Lint PR"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- edited
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
main:
|
|
||||||
name: Validate PR title
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
14
.github/workflows/pre_commit.yml
vendored
14
.github/workflows/pre_commit.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
name: pre-commit
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-commit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-python@v3
|
|
||||||
- uses: pre-commit/action@v3.0.1
|
|
||||||
40
.github/workflows/pytests.yml
vendored
40
.github/workflows/pytests.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Pytests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
checks: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pytest:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.12"]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v4
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install all dependencies
|
|
||||||
run: uv sync --frozen --all-extras --all-groups
|
|
||||||
|
|
||||||
- name: Build goose image
|
|
||||||
run: |
|
|
||||||
uv tool install --with-editable . .
|
|
||||||
cubbi image build goose
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
run: |
|
|
||||||
uv run --frozen -m pytest -v
|
|
||||||
127
.github/workflows/release.yml
vendored
127
.github/workflows/release.yml
vendored
@@ -1,127 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_force:
|
|
||||||
# see https://python-semantic-release.readthedocs.io/en/latest/github-action.html#command-line-options
|
|
||||||
description: |
|
|
||||||
Force release be one of: [major | minor | patch]
|
|
||||||
Leave empty for auto-detect based on commit messages.
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "" # auto - no force
|
|
||||||
- major # force major
|
|
||||||
- minor # force minor
|
|
||||||
- patch # force patch
|
|
||||||
default: ""
|
|
||||||
required: false
|
|
||||||
prerelease_token:
|
|
||||||
description: 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in `1.2.0-rc.8`.'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- rc
|
|
||||||
- beta
|
|
||||||
- alpha
|
|
||||||
default: rc
|
|
||||||
required: false
|
|
||||||
prerelease:
|
|
||||||
description: "Is a pre-release"
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
required: false
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy
|
|
||||||
cancel-in-progress: false # prevent hickups with semantic-release
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION_DEFAULT: "3.12"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency: release
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Note: we need to checkout the repository at the workflow sha in case during the workflow
|
|
||||||
# the branch was updated. To keep PSR working with the configured release branches,
|
|
||||||
# we force a checkout of the desired release branch but at the workflow sha HEAD.
|
|
||||||
- name: Setup | Checkout Repository at workflow sha
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ github.sha }}
|
|
||||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
||||||
|
|
||||||
- name: Setup | Force correct release branch on workflow sha
|
|
||||||
run: |
|
|
||||||
git checkout -B ${{ github.ref_name }} ${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v4
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
|
|
||||||
|
|
||||||
- name: Install all dependencies
|
|
||||||
run: uv sync --frozen --all-extras --all-groups
|
|
||||||
|
|
||||||
# 2 steps to prevent uv.lock out of sync
|
|
||||||
# CF https://github.com/python-semantic-release/python-semantic-release/issues/1125
|
|
||||||
- name: Action | Semantic Version Release (stamp only)
|
|
||||||
uses: python-semantic-release/python-semantic-release@v9.12.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
git_committer_name: "github-actions"
|
|
||||||
git_committer_email: "actions@users.noreply.github.com"
|
|
||||||
force: ${{ github.event.inputs.release_force }}
|
|
||||||
prerelease: ${{ github.event.inputs.prerelease }}
|
|
||||||
prerelease_token: ${{ github.event.inputs.prerelease_token }}
|
|
||||||
ssh_public_signing_key: ${{ secrets.DEPLOY_KEY_PUB }}
|
|
||||||
ssh_private_signing_key: ${{ secrets.DEPLOY_KEY }}
|
|
||||||
push: false
|
|
||||||
commit: false
|
|
||||||
tag: false
|
|
||||||
changelog: false
|
|
||||||
|
|
||||||
- name: Push and tags
|
|
||||||
run: |
|
|
||||||
uv lock
|
|
||||||
git add uv.lock
|
|
||||||
|
|
||||||
- name: Action | Semantic Version Release (fully to create release)
|
|
||||||
id: release
|
|
||||||
uses: python-semantic-release/python-semantic-release@v9.12.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
git_committer_name: "github-actions"
|
|
||||||
git_committer_email: "actions@users.noreply.github.com"
|
|
||||||
force: ${{ github.event.inputs.release_force }}
|
|
||||||
prerelease: ${{ github.event.inputs.prerelease }}
|
|
||||||
prerelease_token: ${{ github.event.inputs.prerelease_token }}
|
|
||||||
ssh_public_signing_key: ${{ secrets.DEPLOY_KEY_PUB }}
|
|
||||||
ssh_private_signing_key: ${{ secrets.DEPLOY_KEY }}
|
|
||||||
push: false
|
|
||||||
|
|
||||||
- name: Push and tags
|
|
||||||
run: |
|
|
||||||
git push --set-upstream --follow-tags origin ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: uv build
|
|
||||||
|
|
||||||
- name: Publish | Upload package to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
if: steps.release.outputs.released == 'true'
|
|
||||||
|
|
||||||
- name: Publish | Upload to GitHub Release Assets
|
|
||||||
uses: python-semantic-release/publish-action@v9.8.9
|
|
||||||
if: steps.release.outputs.released == 'true'
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag: ${{ steps.release.outputs.tag }}
|
|
||||||
159
CHANGELOG.md
159
CHANGELOG.md
@@ -1,159 +0,0 @@
|
|||||||
# CHANGELOG
|
|
||||||
|
|
||||||
|
|
||||||
## v0.1.0-rc.1 (2025-04-18)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* fix: mcp tests ([`3799f04`](https://github.com/Monadical-SAS/cubbi/commit/3799f04c1395d3b018f371db0c0cb8714e6fb8b3))
|
|
||||||
|
|
||||||
* fix: osx tests on volume ([`7fc9cfd`](https://github.com/Monadical-SAS/cubbi/commit/7fc9cfd8e1babfa069691d3b7997449535069674))
|
|
||||||
|
|
||||||
* fix: remove the "mc stop" meant to be in the container, but not implemented ([`4f54c0f`](https://github.com/Monadical-SAS/cubbi/commit/4f54c0fbe7886c8551368b4b35be3ad8c7ae49ab))
|
|
||||||
|
|
||||||
* fix(uid): correctly pass uid/gid to project ([`e25e30e`](https://github.com/Monadical-SAS/cubbi/commit/e25e30e7492c6b0a03017440a18bb2708927fc19))
|
|
||||||
|
|
||||||
* fix(goose): always update the file ([`b1aa415`](https://github.com/Monadical-SAS/cubbi/commit/b1aa415ddee981dc1278cd24f7509363b9c54a54))
|
|
||||||
|
|
||||||
* fix(goose): ensure configuration is run as user ([`cfa7dd6`](https://github.com/Monadical-SAS/cubbi/commit/cfa7dd647d1e4055bf9159be2ee9c2280f2d908e))
|
|
||||||
|
|
||||||
* fix(mcp): fix UnboundLocalError: cannot access local variable 'container_name' where it is not associated with a value ([`deff036`](https://github.com/Monadical-SAS/cubbi/commit/deff036406d72d55659da40520a3a09599d65f07))
|
|
||||||
|
|
||||||
* fix(ssh): do not enable ssh automatically ([`f32b3dd`](https://github.com/Monadical-SAS/cubbi/commit/f32b3dd269d1a3d6ebaa2e7b2893f267b5175b20))
|
|
||||||
|
|
||||||
* fix(uid): use symlink instead of volume for persistent volume in the container ([`a74251b`](https://github.com/Monadical-SAS/cubbi/commit/a74251b119d24714c7cc1eaadeea851008006137))
|
|
||||||
|
|
||||||
* fix(session): ensure a session connect only to the mcp server passed in --mcp ([`5d674f7`](https://github.com/Monadical-SAS/cubbi/commit/5d674f750878f0895dc1544620e8b1da4da29752))
|
|
||||||
|
|
||||||
* fix(goose): add ping, nano and vim to the default image ([`028bd26`](https://github.com/Monadical-SAS/cubbi/commit/028bd26cf12e181541e006650b58d97e1d568a45))
|
|
||||||
|
|
||||||
* fix(goose): install latest goose version, do not use pip ([`7649173`](https://github.com/Monadical-SAS/cubbi/commit/7649173d6c8a82ac236d0f89263591eaa6e21a20))
|
|
||||||
|
|
||||||
* fix(mc): fix runtime issue when starting mc ([`6f08e2b`](https://github.com/Monadical-SAS/cubbi/commit/6f08e2b274b67001694123b5bb977401df0810c6))
|
|
||||||
|
|
||||||
* fix(session): fix session status display ([`092f497`](https://github.com/Monadical-SAS/cubbi/commit/092f497ecc19938d4917a18441995170d1f68704))
|
|
||||||
|
|
||||||
* fix(goose): remove MCP_HOST and such, this is not how mcp works ([`d42af87`](https://github.com/Monadical-SAS/cubbi/commit/d42af870ff56112b4503f2568b8a5b0f385c435c))
|
|
||||||
|
|
||||||
* fix(langfuse): fix goose langfuse integration (wrong env variables) ([`e36eef4`](https://github.com/Monadical-SAS/cubbi/commit/e36eef4ef7c2d0cbdef31704afb45c50c4293986))
|
|
||||||
|
|
||||||
* fix: remove double connecting to message ([`e36f454`](https://github.com/Monadical-SAS/cubbi/commit/e36f4540bfe3794ab2d065f552cfb9528489de71))
|
|
||||||
|
|
||||||
* fix(cli): rename MAI->MC ([`354834f`](https://github.com/Monadical-SAS/cubbi/commit/354834fff733c37202b01a6fc49ebdf5003390c1))
|
|
||||||
|
|
||||||
* fix(goose): rename mai to mc, add initialization status ([`74c723d`](https://github.com/Monadical-SAS/cubbi/commit/74c723db7b6b7dd57c4ca32a804436a990e5260c))
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* chore: remove unnecessary output ([`30c6b99`](https://github.com/Monadical-SAS/cubbi/commit/30c6b995cbb5bdf3dc7adf2e79d8836660d4f295))
|
|
||||||
|
|
||||||
* chore: update doc and add pre-commit ([`958d87b`](https://github.com/Monadical-SAS/cubbi/commit/958d87bcaeed16210a7c22574b5e63f2422af098))
|
|
||||||
|
|
||||||
### Continuous Integration
|
|
||||||
|
|
||||||
* ci: add ci files (#11)
|
|
||||||
|
|
||||||
* ci: add ci files
|
|
||||||
|
|
||||||
* fix: add goose image build ([`3850bc3`](https://github.com/Monadical-SAS/cubbi/commit/3850bc32129da539f53b69427ddca85f8c5f390a))
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
* docs: Prefer mcx alias in README examples ([`9c21611`](https://github.com/Monadical-SAS/cubbi/commit/9c21611a7fa1497f7cbddb1f1b4cd22b4ebc8a19))
|
|
||||||
|
|
||||||
* docs: Add --run option examples to README ([`6b2c1eb`](https://github.com/Monadical-SAS/cubbi/commit/6b2c1ebf1cd7a5d9970234112f32fe7a231303f9))
|
|
||||||
|
|
||||||
* docs(mcp): add specification for MCP server support ([`20916c5`](https://github.com/Monadical-SAS/cubbi/commit/20916c5713b3a047f4a8a33194f751f36e3c8a7a))
|
|
||||||
|
|
||||||
* docs(readme): remove license part ([`1c538f8`](https://github.com/Monadical-SAS/cubbi/commit/1c538f8a59e28888309c181ae8f8034b9e70a631))
|
|
||||||
|
|
||||||
* docs(readme): update README to update tool call ([`a4591dd`](https://github.com/Monadical-SAS/cubbi/commit/a4591ddbd863bc6658a7643d3f33d06c82816cae))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* feat(project): explicitely add --project to save information in /mc-config across run.
|
|
||||||
|
|
||||||
Containers are now isolated by default. ([`3a182fd`](https://github.com/Monadical-SAS/cubbi/commit/3a182fd2658c0eb361ce5ed88938686e2bd19e59))
|
|
||||||
|
|
||||||
* feat(gemini): support for gemini model ([`2f9fd68`](https://github.com/Monadical-SAS/cubbi/commit/2f9fd68cada9b5aaba652efb67368c2641046da5))
|
|
||||||
|
|
||||||
* feat(llm): add default model/provider to auto configure the driver (#7) ([`5b9713d`](https://github.com/Monadical-SAS/cubbi/commit/5b9713dc2f7d7c25808ad37094838c697c056fec))
|
|
||||||
|
|
||||||
* feat(goose): update config using uv script with pyyaml (#6) ([`9e742b4`](https://github.com/Monadical-SAS/cubbi/commit/9e742b439b7b852efa4219850f8b67c143274045))
|
|
||||||
|
|
||||||
* feat(ssh): make SSH server optional with --ssh flag
|
|
||||||
|
|
||||||
- Added --ssh flag to session create command
|
|
||||||
- Modified mc-init.sh to check MC_SSH_ENABLED environment variable
|
|
||||||
- SSH server is now disabled by default
|
|
||||||
- Updated README.md with new flag example
|
|
||||||
- Fixed UnboundLocalError with container_name in exception handler ([`5678438`](https://github.com/Monadical-SAS/cubbi/commit/56784386614fcd0a52be8a2eb89d2deef9323ca1))
|
|
||||||
|
|
||||||
* feat(run): add --run command ([`33d90d0`](https://github.com/Monadical-SAS/cubbi/commit/33d90d05311ad872b7a7d4cd303ff6f7b7726038))
|
|
||||||
|
|
||||||
* feat(mc): support for uid/gid, and use default current user ([`a51115a`](https://github.com/Monadical-SAS/cubbi/commit/a51115a45d88bf703fb5380171042276873b7207))
|
|
||||||
|
|
||||||
* feat(mcp): ensure inner mcp environemnt variables are passed ([`0d75bfc`](https://github.com/Monadical-SAS/cubbi/commit/0d75bfc3d8e130fb05048c2bc8a674f6b7e5de83))
|
|
||||||
|
|
||||||
* feat(goose): auto add mcp server to goose configuration when starting a session ([`7805aa7`](https://github.com/Monadical-SAS/cubbi/commit/7805aa720eba78d47f2ad565f6944e84a21c4b1c))
|
|
||||||
|
|
||||||
* feat(goose): optimize init status ([`16f59b1`](https://github.com/Monadical-SAS/cubbi/commit/16f59b1c408dbff4781ad7ccfa70e81d6d98f7bd))
|
|
||||||
|
|
||||||
* feat(mcp): add the possibility to have default mcp to connect to ([`4b0461a`](https://github.com/Monadical-SAS/cubbi/commit/4b0461a6faf81de1e1b54d1fe78fea7977cde9dd))
|
|
||||||
|
|
||||||
* feat(mcp): improve inspector reliability over re-run ([`3ee8ce6`](https://github.com/Monadical-SAS/cubbi/commit/3ee8ce6338c35b7e48d788d2dddfa9b6a70381cb))
|
|
||||||
|
|
||||||
* feat(mcp): add inspector ([`d098f26`](https://github.com/Monadical-SAS/cubbi/commit/d098f268cd164e9d708089c9f9525a940653c010))
|
|
||||||
|
|
||||||
* feat(mcp): first docker proxy working ([`0892b6c`](https://github.com/Monadical-SAS/cubbi/commit/0892b6c8c472063c639cc78cf29b322bb39f998f))
|
|
||||||
|
|
||||||
* feat(mcp): initial version of mcp ([`212f271`](https://github.com/Monadical-SAS/cubbi/commit/212f271268c5724775beceae119f97aec2748dcb))
|
|
||||||
|
|
||||||
* feat(volume): add mc config volume command ([`2caeb42`](https://github.com/Monadical-SAS/cubbi/commit/2caeb425518242fbe1c921b9678e6e7571b9b0a6))
|
|
||||||
|
|
||||||
* feat(config): ensure config is correctly saved ([`deb5945`](https://github.com/Monadical-SAS/cubbi/commit/deb5945e40d55643dca4e1aa4201dfa8da1bfd70))
|
|
||||||
|
|
||||||
* feat(cli): separate session state into its own session.yaml file ([`7736573`](https://github.com/Monadical-SAS/cubbi/commit/7736573b84c7a51eaa60b932f835726b411ca742))
|
|
||||||
|
|
||||||
* feat(cli): support to join external network ([`133583b`](https://github.com/Monadical-SAS/cubbi/commit/133583b941ed56d1b0636277bb847c45eee7f3b8))
|
|
||||||
|
|
||||||
* feat(volume): add the possibilty to mount local directory into the container (like docker volume) ([`b72f1ee`](https://github.com/Monadical-SAS/cubbi/commit/b72f1eef9af598f2090a0edae8921c16814b3cda))
|
|
||||||
|
|
||||||
* feat(config): add global user configuration for the tool
|
|
||||||
|
|
||||||
- langfuse
|
|
||||||
- default driver
|
|
||||||
- and api keys ([`dab783b`](https://github.com/Monadical-SAS/cubbi/commit/dab783b01d82bcb210b5e01ac3b93ba64c7bc023))
|
|
||||||
|
|
||||||
* feat(keys): pass local keys to the session by default ([`f83c49c`](https://github.com/Monadical-SAS/cubbi/commit/f83c49c0f340d1a3accba1fe1317994b492755c0))
|
|
||||||
|
|
||||||
* feat(cli): more information when closing session ([`08ba1ab`](https://github.com/Monadical-SAS/cubbi/commit/08ba1ab2da3c24237c0f0bc411924d8ffbe71765))
|
|
||||||
|
|
||||||
* feat(cli): auto mount current directory as /app ([`e6e3c20`](https://github.com/Monadical-SAS/cubbi/commit/e6e3c207bcee531b135824688adf1a56ae427a01))
|
|
||||||
|
|
||||||
* feat(cli): auto connect to a session ([`4a63606`](https://github.com/Monadical-SAS/cubbi/commit/4a63606d58cc3e331a349974e9b3bf2d856a72a1))
|
|
||||||
|
|
||||||
* feat(cli): phase 1 - local cli with docker integration ([`6443083`](https://github.com/Monadical-SAS/cubbi/commit/64430830d883308e4d52e17b25c260a0d5385141))
|
|
||||||
|
|
||||||
* feat: first commit ([`fde6529`](https://github.com/Monadical-SAS/cubbi/commit/fde6529d545b5625484c5c1236254d2e0c6f0f4d))
|
|
||||||
|
|
||||||
### Refactoring
|
|
||||||
|
|
||||||
* refactor: rename project to cubbi ([`12d77d0`](https://github.com/Monadical-SAS/cubbi/commit/12d77d0128e4d82e5ddc1a4ab7e873ddaa22e130))
|
|
||||||
|
|
||||||
* refactor: rename driver to image, first pass ([`51fb79b`](https://github.com/Monadical-SAS/cubbi/commit/51fb79baa30ff479ac5479ba5ea0cad70bbb4c20))
|
|
||||||
|
|
||||||
* refactor: reduce amount of data in session.yaml ([`979b438`](https://github.com/Monadical-SAS/cubbi/commit/979b43846a798f1fb25ff05e6dc1fc27fa16f590))
|
|
||||||
|
|
||||||
* refactor: move drivers directory into mcontainer package
|
|
||||||
|
|
||||||
- Relocate goose driver to mcontainer/drivers/
|
|
||||||
- Update ConfigManager to dynamically scan for driver YAML files
|
|
||||||
- Add support for mc-driver.yaml instead of mai-driver.yaml
|
|
||||||
- Update Driver model to support init commands and other YAML fields
|
|
||||||
- Auto-discover drivers at runtime instead of hardcoding them
|
|
||||||
- Update documentation to reflect new directory structure ([`307eee4`](https://github.com/Monadical-SAS/cubbi/commit/307eee4fcef47189a98a76187d6080a36423ad6e))
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
* test: add unit tests ([`7c46d66`](https://github.com/Monadical-SAS/cubbi/commit/7c46d66b53ac49c08458bc5d72e636e7d296e74f))
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
# Cubbi Container Development Guide
|
# Monadical Container Development Guide
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies using uv (Astral)
|
# Install dependencies using uv (Astral)
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
# Run Cubbi CLI
|
# Run MC service
|
||||||
uv run -m cubbi.cli
|
uv run -m mcontainer.service
|
||||||
|
|
||||||
|
# Run MC CLI
|
||||||
|
uv run -m mcontainer.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lint/Test Commands
|
## Lint/Test Commands
|
||||||
|
|||||||
224
README.md
224
README.md
@@ -1,136 +1,124 @@
|
|||||||
<div align="center">
|
# MC - Monadical Container Tool
|
||||||
|
|
||||||
# Cubbi - Container Tool
|
MC (Monadical Container) is a command-line tool for managing ephemeral
|
||||||
|
containers that run AI tools and development environments. It works with both
|
||||||
|
local Docker and a dedicated remote web service that manages containers in a
|
||||||
|
Docker-in-Docker (DinD) environment. MC also supports connecting to MCP (Model Context Protocol) servers to extend AI tools with additional capabilities.
|
||||||
|
|
||||||
Cubbi is a command-line tool for managing ephemeral containers that run AI tools and development environments. It works with both local Docker and a dedicated remote web service that manages containers in a Docker-in-Docker (DinD) environment. Cubbi also supports connecting to MCP (Model Control Protocol) servers to extend AI tools with additional capabilities.
|
## Quick Reference
|
||||||
|
|
||||||

|
- `mc session create` - Create a new session
|
||||||

|
- `mcx` - Shortcut for `mc session create`
|
||||||
[](https://github.com/monadical-sas/cubbi/actions/workflows/pytests.yml)
|
- `mcx .` - Mount the current directory
|
||||||
|
- `mcx /path/to/dir` - Mount a specific directory
|
||||||
|
- `mcx https://github.com/user/repo` - Clone a repository
|
||||||
|
|
||||||
</div>
|
## Requirements
|
||||||
|
|
||||||
## 🚀 Quick Reference
|
|
||||||
|
|
||||||
- `cubbi session create` - Create a new session
|
|
||||||
- `cubbix` - Shortcut for `cubbi session create`
|
|
||||||
- `cubbix .` - Mount the current directory
|
|
||||||
- `cubbix /path/to/dir` - Mount a specific directory
|
|
||||||
- `cubbix https://github.com/user/repo` - Clone a repository
|
|
||||||
|
|
||||||
## 📋 Requirements
|
|
||||||
|
|
||||||
- [uv](https://docs.astral.sh/uv/)
|
- [uv](https://docs.astral.sh/uv/)
|
||||||
|
|
||||||
## 📥 Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/monadical/cubbi.git
|
git clone https://github.com/monadical/mcontainer.git
|
||||||
|
|
||||||
# Install the tool locally
|
# Install the tool locally
|
||||||
# (with editable, so you can update the code and work with it)
|
# (with editable, so you can update the code and work with it)
|
||||||
cd cubbi
|
cd mcontainer
|
||||||
uv tool install --with-editable . .
|
uv tool install --with-editable . .
|
||||||
|
|
||||||
# Then you could use the tool as `cubbi`
|
# Then you could use the tool as `mc`
|
||||||
cubbi --help
|
mc --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Important: compile your first image
|
## Basic Usage
|
||||||
|
|
||||||
```bash
|
|
||||||
cubbi image build goose
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Basic Usage
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Show help message (displays available commands)
|
# Show help message (displays available commands)
|
||||||
cubbi
|
mc
|
||||||
|
|
||||||
# Create a new session with the default image (using cubbix alias)
|
# Create a new session with the default driver (using mcx alias)
|
||||||
cubbix
|
mcx
|
||||||
|
|
||||||
# Create a session and run an initial command before the shell starts
|
# Create a session and run an initial command before the shell starts
|
||||||
cubbix --run "echo 'Setup complete'; ls -l"
|
mcx --run "echo 'Setup complete'; ls -l"
|
||||||
|
|
||||||
# List all active sessions
|
# List all active sessions
|
||||||
cubbi session list
|
mc session list
|
||||||
|
|
||||||
# Connect to a specific session
|
# Connect to a specific session
|
||||||
cubbi session connect SESSION_ID
|
mc session connect SESSION_ID
|
||||||
|
|
||||||
# Close a session when done
|
# Close a session when done
|
||||||
cubbi session close SESSION_ID
|
mc session close SESSION_ID
|
||||||
|
|
||||||
# Create a session with a specific image
|
# Create a session with a specific driver
|
||||||
cubbix --image goose
|
mcx --driver goose
|
||||||
|
|
||||||
# Create a session with environment variables
|
# Create a session with environment variables
|
||||||
cubbix -e VAR1=value1 -e VAR2=value2
|
mcx -e VAR1=value1 -e VAR2=value2
|
||||||
|
|
||||||
# Mount custom volumes (similar to Docker's -v flag)
|
# Mount custom volumes (similar to Docker's -v flag)
|
||||||
cubbix -v /local/path:/container/path
|
mcx -v /local/path:/container/path
|
||||||
cubbix -v ~/data:/data -v ./configs:/etc/app/config
|
mcx -v ~/data:/data -v ./configs:/etc/app/config
|
||||||
|
|
||||||
# Mount a local directory (current directory or specific path)
|
# Mount a local directory (current directory or specific path)
|
||||||
cubbix .
|
mcx .
|
||||||
cubbix /path/to/project
|
mcx /path/to/project
|
||||||
|
|
||||||
# Connect to external Docker networks
|
# Connect to external Docker networks
|
||||||
cubbix --network teamnet --network dbnet
|
mcx --network teamnet --network dbnet
|
||||||
|
|
||||||
# Connect to MCP servers for extended capabilities
|
# Connect to MCP servers for extended capabilities
|
||||||
cubbix --mcp github --mcp jira
|
mcx --mcp github --mcp jira
|
||||||
|
|
||||||
# Clone a Git repository
|
# Clone a Git repository
|
||||||
cubbix https://github.com/username/repo
|
mcx https://github.com/username/repo
|
||||||
|
|
||||||
# Using the cubbix shortcut (equivalent to cubbi session create)
|
# Using the mcx shortcut (equivalent to mc session create)
|
||||||
cubbix # Creates a session without mounting anything
|
mcx # Creates a session without mounting anything
|
||||||
cubbix . # Mounts the current directory
|
mcx . # Mounts the current directory
|
||||||
cubbix /path/to/project # Mounts the specified directory
|
mcx /path/to/project # Mounts the specified directory
|
||||||
cubbix https://github.com/username/repo # Clones the repository
|
mcx https://github.com/username/repo # Clones the repository
|
||||||
|
|
||||||
# Shorthand with MCP servers
|
# Shorthand with MCP servers
|
||||||
cubbix https://github.com/username/repo --mcp github
|
mcx https://github.com/username/repo --mcp github
|
||||||
|
|
||||||
# Shorthand with an initial command
|
# Shorthand with an initial command
|
||||||
cubbix . --run "apt-get update && apt-get install -y my-package"
|
mcx . --run "apt-get update && apt-get install -y my-package"
|
||||||
|
|
||||||
# Enable SSH server in the container
|
# Enable SSH server in the container
|
||||||
cubbix --ssh
|
mcx --ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🖼️ Image Management
|
## Driver Management
|
||||||
|
|
||||||
Cubbi includes an image management system that allows you to build, manage, and use Docker images for different AI tools:
|
MC includes a driver management system that allows you to build, manage, and use Docker images for different AI tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List available images
|
# List available drivers
|
||||||
cubbi image list
|
mc driver list
|
||||||
|
|
||||||
# Get detailed information about an image
|
# Get detailed information about a driver
|
||||||
cubbi image info goose
|
mc driver info goose
|
||||||
|
|
||||||
# Build an image
|
# Build a driver image
|
||||||
cubbi image build goose
|
mc driver build goose
|
||||||
|
|
||||||
# Build and push an image
|
# Build and push a driver image
|
||||||
cubbi image build goose --push
|
mc driver build goose --push
|
||||||
```
|
```
|
||||||
|
|
||||||
Images are defined in the `cubbi/images/` directory, with each subdirectory containing:
|
Drivers are defined in the `mcontainer/drivers/` directory, with each subdirectory containing:
|
||||||
|
|
||||||
- `Dockerfile`: Docker image definition
|
- `Dockerfile`: Docker image definition
|
||||||
- `entrypoint.sh`: Container entrypoint script
|
- `entrypoint.sh`: Container entrypoint script
|
||||||
- `cubbi-init.sh`: Standardized initialization script
|
- `mc-init.sh`: Standardized initialization script
|
||||||
- `cubbi-image.yaml`: Image metadata and configuration
|
- `mc-driver.yaml`: Driver metadata and configuration
|
||||||
- `README.md`: Image documentation
|
- `README.md`: Driver documentation
|
||||||
|
|
||||||
Cubbi automatically discovers and loads image definitions from the YAML files.
|
MC automatically discovers and loads driver definitions from the YAML files.
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -148,30 +136,30 @@ uvx mypy .
|
|||||||
uvx ruff format .
|
uvx ruff format .
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## Configuration
|
||||||
|
|
||||||
Cubbi supports user-specific configuration via a YAML file located at `~/.config/cubbi/config.yaml`. This allows you to set default values and configure service credentials.
|
MC supports user-specific configuration via a YAML file located at `~/.config/mc/config.yaml`. This allows you to set default values and configure service credentials.
|
||||||
|
|
||||||
### Managing Configuration
|
### Managing Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View all configuration
|
# View all configuration
|
||||||
cubbi config list
|
mc config list
|
||||||
|
|
||||||
# Get a specific configuration value
|
# Get a specific configuration value
|
||||||
cubbi config get langfuse.url
|
mc config get langfuse.url
|
||||||
|
|
||||||
# Set configuration values
|
# Set configuration values
|
||||||
cubbi config set langfuse.url "https://cloud.langfuse.com"
|
mc config set langfuse.url "https://cloud.langfuse.com"
|
||||||
cubbi config set langfuse.public_key "pk-lf-..."
|
mc config set langfuse.public_key "pk-lf-..."
|
||||||
cubbi config set langfuse.secret_key "sk-lf-..."
|
mc config set langfuse.secret_key "sk-lf-..."
|
||||||
|
|
||||||
# Set API keys for various services
|
# Set API keys for various services
|
||||||
cubbi config set openai.api_key "sk-..."
|
mc config set openai.api_key "sk-..."
|
||||||
cubbi config set anthropic.api_key "sk-ant-..."
|
mc config set anthropic.api_key "sk-ant-..."
|
||||||
|
|
||||||
# Reset configuration to defaults
|
# Reset configuration to defaults
|
||||||
cubbi config reset
|
mc config reset
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default Networks Configuration
|
### Default Networks Configuration
|
||||||
@@ -180,13 +168,13 @@ You can configure default networks that will be applied to every new session:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List default networks
|
# List default networks
|
||||||
cubbi config network list
|
mc config network list
|
||||||
|
|
||||||
# Add a network to defaults
|
# Add a network to defaults
|
||||||
cubbi config network add teamnet
|
mc config network add teamnet
|
||||||
|
|
||||||
# Remove a network from defaults
|
# Remove a network from defaults
|
||||||
cubbi config network remove teamnet
|
mc config network remove teamnet
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default Volumes Configuration
|
### Default Volumes Configuration
|
||||||
@@ -195,13 +183,13 @@ You can configure default volumes that will be automatically mounted in every ne
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List default volumes
|
# List default volumes
|
||||||
cubbi config volume list
|
mc config volume list
|
||||||
|
|
||||||
# Add a volume to defaults
|
# Add a volume to defaults
|
||||||
cubbi config volume add /local/path:/container/path
|
mc config volume add /local/path:/container/path
|
||||||
|
|
||||||
# Remove a volume from defaults (will prompt if multiple matches found)
|
# Remove a volume from defaults (will prompt if multiple matches found)
|
||||||
cubbi config volume remove /local/path
|
mc config volume remove /local/path
|
||||||
```
|
```
|
||||||
|
|
||||||
Default volumes will be combined with any volumes specified using the `-v` flag when creating a session.
|
Default volumes will be combined with any volumes specified using the `-v` flag when creating a session.
|
||||||
@@ -212,35 +200,35 @@ You can configure default MCP servers that sessions will automatically connect t
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List default MCP servers
|
# List default MCP servers
|
||||||
cubbi config mcp list
|
mc config mcp list
|
||||||
|
|
||||||
# Add an MCP server to defaults
|
# Add an MCP server to defaults
|
||||||
cubbi config mcp add github
|
mc config mcp add github
|
||||||
|
|
||||||
# Remove an MCP server from defaults
|
# Remove an MCP server from defaults
|
||||||
cubbi config mcp remove github
|
mc config mcp remove github
|
||||||
```
|
```
|
||||||
|
|
||||||
When adding new MCP servers, they are added to defaults by default. Use the `--no-default` flag to prevent this:
|
When adding new MCP servers, they are added to defaults by default. Use the `--no-default` flag to prevent this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add an MCP server without adding it to defaults
|
# Add an MCP server without adding it to defaults
|
||||||
cubbi mcp add github ghcr.io/mcp/github:latest --no-default
|
mc mcp add github ghcr.io/mcp/github:latest --no-default
|
||||||
cubbi mcp add-remote jira https://jira-mcp.example.com/sse --no-default
|
mc mcp add-remote jira https://jira-mcp.example.com/sse --no-default
|
||||||
```
|
```
|
||||||
|
|
||||||
When creating sessions, if no MCP server is specified with `--mcp`, the default MCP servers will be used automatically.
|
When creating sessions, if no MCP server is specified with `--mcp`, the default MCP servers will be used automatically.
|
||||||
|
|
||||||
### External Network Connectivity
|
### External Network Connectivity
|
||||||
|
|
||||||
Cubbi containers can connect to external Docker networks, allowing them to communicate with other services in those networks:
|
MC containers can connect to external Docker networks, allowing them to communicate with other services in those networks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a session connected to external networks
|
# Create a session connected to external networks
|
||||||
cubbi session create --network teamnet --network dbnet
|
mc session create --network teamnet --network dbnet
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important**: Networks must be "attachable" to be joined by Cubbi containers. Here's how to create attachable networks:
|
**Important**: Networks must be "attachable" to be joined by MC containers. Here's how to create attachable networks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create an attachable network with Docker
|
# Create an attachable network with Docker
|
||||||
@@ -258,12 +246,12 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
teamnet:
|
teamnet:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
attachable: true # This is required for Cubbi containers to connect
|
attachable: true # This is required for MC containers to connect
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Credentials
|
### Service Credentials
|
||||||
|
|
||||||
Service credentials like API keys configured in `~/.config/cubbi/config.yaml` are automatically passed to containers as environment variables:
|
Service credentials like API keys configured in `~/.config/mc/config.yaml` are automatically passed to containers as environment variables:
|
||||||
|
|
||||||
| Config Setting | Environment Variable |
|
| Config Setting | Environment Variable |
|
||||||
|----------------|---------------------|
|
|----------------|---------------------|
|
||||||
@@ -275,9 +263,9 @@ Service credentials like API keys configured in `~/.config/cubbi/config.yaml` ar
|
|||||||
| `openrouter.api_key` | `OPENROUTER_API_KEY` |
|
| `openrouter.api_key` | `OPENROUTER_API_KEY` |
|
||||||
| `google.api_key` | `GOOGLE_API_KEY` |
|
| `google.api_key` | `GOOGLE_API_KEY` |
|
||||||
|
|
||||||
## 🌐 MCP Server Management
|
## MCP Server Management
|
||||||
|
|
||||||
MCP (Model Control Protocol) servers provide tool-calling capabilities to AI models, enhancing their ability to interact with external services, databases, and systems. Cubbi supports multiple types of MCP servers:
|
MCP (Model Context Protocol) servers provide tool-calling capabilities to AI models, enhancing their ability to interact with external services, databases, and systems. MC supports multiple types of MCP servers:
|
||||||
|
|
||||||
1. **Remote HTTP SSE servers** - External MCP servers accessed over HTTP
|
1. **Remote HTTP SSE servers** - External MCP servers accessed over HTTP
|
||||||
2. **Docker-based MCP servers** - Local MCP servers running in Docker containers
|
2. **Docker-based MCP servers** - Local MCP servers running in Docker containers
|
||||||
@@ -287,56 +275,56 @@ MCP (Model Control Protocol) servers provide tool-calling capabilities to AI mod
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all configured MCP servers and their status
|
# List all configured MCP servers and their status
|
||||||
cubbi mcp list
|
mc mcp list
|
||||||
|
|
||||||
# View detailed status of an MCP server
|
# View detailed status of an MCP server
|
||||||
cubbi mcp status github
|
mc mcp status github
|
||||||
|
|
||||||
# Start/stop/restart individual MCP servers
|
# Start/stop/restart individual MCP servers
|
||||||
cubbi mcp start github
|
mc mcp start github
|
||||||
cubbi mcp stop github
|
mc mcp stop github
|
||||||
cubbi mcp restart github
|
mc mcp restart github
|
||||||
|
|
||||||
# Start all MCP servers at once
|
# Start all MCP servers at once
|
||||||
cubbi mcp start --all
|
mc mcp start --all
|
||||||
|
|
||||||
# Stop and remove all MCP servers at once
|
# Stop and remove all MCP servers at once
|
||||||
cubbi mcp stop --all
|
mc mcp stop --all
|
||||||
|
|
||||||
# Run the MCP Inspector to visualize and interact with MCP servers
|
# Run the MCP Inspector to visualize and interact with MCP servers
|
||||||
# It automatically joins all MCP networks for seamless DNS resolution
|
# It automatically joins all MCP networks for seamless DNS resolution
|
||||||
# Uses two ports: frontend UI (default: 5173) and backend API (default: 3000)
|
# Uses two ports: frontend UI (default: 5173) and backend API (default: 3000)
|
||||||
cubbi mcp inspector
|
mc mcp inspector
|
||||||
|
|
||||||
# Run the MCP Inspector with custom ports
|
# Run the MCP Inspector with custom ports
|
||||||
cubbi mcp inspector --client-port 6173 --server-port 6174
|
mc mcp inspector --client-port 6173 --server-port 6174
|
||||||
|
|
||||||
# Run the MCP Inspector in detached mode
|
# Run the MCP Inspector in detached mode
|
||||||
cubbi mcp inspector --detach
|
mc mcp inspector --detach
|
||||||
|
|
||||||
# Stop the MCP Inspector
|
# Stop the MCP Inspector
|
||||||
cubbi mcp inspector --stop
|
mc mcp inspector --stop
|
||||||
|
|
||||||
# View MCP server logs
|
# View MCP server logs
|
||||||
cubbi mcp logs github
|
mc mcp logs github
|
||||||
|
|
||||||
# Remove an MCP server configuration
|
# Remove an MCP server configuration
|
||||||
cubbi mcp remove github
|
mc mcp remove github
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding MCP Servers
|
### Adding MCP Servers
|
||||||
|
|
||||||
Cubbi supports different types of MCP servers:
|
MC supports different types of MCP servers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add a remote HTTP SSE MCP server
|
# Add a remote HTTP SSE MCP server
|
||||||
cubbi mcp remote add github http://my-mcp-server.example.com/sse --header "Authorization=Bearer token123"
|
mc mcp remote add github http://my-mcp-server.example.com/sse --header "Authorization=Bearer token123"
|
||||||
|
|
||||||
# Add a Docker-based MCP server
|
# Add a Docker-based MCP server
|
||||||
cubbi mcp docker add github mcp/github:latest --command "github-mcp" --env GITHUB_TOKEN=ghp_123456
|
mc mcp docker add github mcp/github:latest --command "github-mcp" --env GITHUB_TOKEN=ghp_123456
|
||||||
|
|
||||||
# Add a proxy-based MCP server (for stdio-to-SSE conversion)
|
# Add a proxy-based MCP server (for stdio-to-SSE conversion)
|
||||||
cubbi mcp add github ghcr.io/mcp/github:latest --proxy-image ghcr.io/sparfenyuk/mcp-proxy:latest --command "github-mcp" --sse-port 8080 --no-default
|
mc mcp add github ghcr.io/mcp/github:latest --proxy-image ghcr.io/sparfenyuk/mcp-proxy:latest --command "github-mcp" --sse-port 8080 --no-default
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using MCP Servers with Sessions
|
### Using MCP Servers with Sessions
|
||||||
@@ -345,13 +333,13 @@ MCP servers can be attached to sessions when they are created:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a session with a single MCP server
|
# Create a session with a single MCP server
|
||||||
cubbi session create --mcp github
|
mc session create --mcp github
|
||||||
|
|
||||||
# Create a session with multiple MCP servers
|
# Create a session with multiple MCP servers
|
||||||
cubbi session create --mcp github --mcp jira
|
mc session create --mcp github --mcp jira
|
||||||
|
|
||||||
# Using MCP with a project repository
|
# Using MCP with a project repository
|
||||||
cubbi github.com/username/repo --mcp github
|
mc github.com/username/repo --mcp github
|
||||||
```
|
```
|
||||||
|
|
||||||
MCP servers are persistent and can be shared between sessions. They continue running even when sessions are closed, allowing for efficient reuse across multiple sessions.
|
MCP servers are persistent and can be shared between sessions. They continue running even when sessions are closed, allowing for efficient reuse across multiple sessions.
|
||||||
|
|||||||
174
cubbi/config.py
174
cubbi/config.py
@@ -1,174 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from .models import Config, Image
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "cubbi"
|
|
||||||
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
|
||||||
DEFAULT_IMAGES_DIR = Path.home() / ".config" / "cubbi" / "images"
|
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent
|
|
||||||
BUILTIN_IMAGES_DIR = Path(__file__).parent / "images"
|
|
||||||
|
|
||||||
# Dynamically loaded from images directory at runtime
|
|
||||||
DEFAULT_IMAGES = {}
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
|
||||||
def __init__(self, config_path: Optional[Path] = None):
|
|
||||||
self.config_path = config_path or DEFAULT_CONFIG_FILE
|
|
||||||
self.config_dir = self.config_path.parent
|
|
||||||
self.images_dir = DEFAULT_IMAGES_DIR
|
|
||||||
self.config = self._load_or_create_config()
|
|
||||||
|
|
||||||
# Always load package images on initialization
|
|
||||||
# These are separate from the user config
|
|
||||||
self.builtin_images = self._load_package_images()
|
|
||||||
|
|
||||||
def _load_or_create_config(self) -> Config:
|
|
||||||
"""Load existing config or create a new one with defaults"""
|
|
||||||
if self.config_path.exists():
|
|
||||||
try:
|
|
||||||
with open(self.config_path, "r") as f:
|
|
||||||
config_data = yaml.safe_load(f) or {}
|
|
||||||
|
|
||||||
# Create a new config from scratch, then update with data from file
|
|
||||||
config = Config(
|
|
||||||
docker=config_data.get("docker", {}),
|
|
||||||
defaults=config_data.get("defaults", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add images
|
|
||||||
if "images" in config_data:
|
|
||||||
for image_name, image_data in config_data["images"].items():
|
|
||||||
config.images[image_name] = Image.model_validate(image_data)
|
|
||||||
|
|
||||||
return config
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading config: {e}")
|
|
||||||
return self._create_default_config()
|
|
||||||
else:
|
|
||||||
return self._create_default_config()
|
|
||||||
|
|
||||||
def _create_default_config(self) -> Config:
|
|
||||||
"""Create a default configuration"""
|
|
||||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
self.images_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Initial config without images
|
|
||||||
config = Config(
|
|
||||||
docker={
|
|
||||||
"socket": "/var/run/docker.sock",
|
|
||||||
"network": "cubbi-network",
|
|
||||||
},
|
|
||||||
defaults={
|
|
||||||
"image": "goose",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.save_config(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
def save_config(self, config: Optional[Config] = None) -> None:
|
|
||||||
"""Save the current config to disk"""
|
|
||||||
if config:
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Use model_dump with mode="json" for proper serialization of enums
|
|
||||||
config_dict = self.config.model_dump(mode="json")
|
|
||||||
|
|
||||||
# Write to file
|
|
||||||
with open(self.config_path, "w") as f:
|
|
||||||
yaml.dump(config_dict, f)
|
|
||||||
|
|
||||||
def get_image(self, name: str) -> Optional[Image]:
|
|
||||||
"""Get an image by name, checking builtin images first, then user-configured ones"""
|
|
||||||
# Check builtin images first (package images take precedence)
|
|
||||||
if name in self.builtin_images:
|
|
||||||
return self.builtin_images[name]
|
|
||||||
# If not found, check user-configured images
|
|
||||||
return self.config.images.get(name)
|
|
||||||
|
|
||||||
def list_images(self) -> Dict[str, Image]:
|
|
||||||
"""List all available images (both builtin and user-configured)"""
|
|
||||||
# Start with user config images
|
|
||||||
all_images = dict(self.config.images)
|
|
||||||
|
|
||||||
# Add builtin images, overriding any user images with the same name
|
|
||||||
# This ensures that package-provided images always take precedence
|
|
||||||
all_images.update(self.builtin_images)
|
|
||||||
|
|
||||||
return all_images
|
|
||||||
|
|
||||||
# Session management has been moved to SessionManager in session.py
|
|
||||||
|
|
||||||
def load_image_from_dir(self, image_dir: Path) -> Optional[Image]:
|
|
||||||
"""Load an image configuration from a directory"""
|
|
||||||
# Check for image config file
|
|
||||||
yaml_path = image_dir / "cubbi-image.yaml"
|
|
||||||
if not yaml_path.exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(yaml_path, "r") as f:
|
|
||||||
image_data = yaml.safe_load(f)
|
|
||||||
|
|
||||||
# Extract required fields
|
|
||||||
if not all(
|
|
||||||
k in image_data
|
|
||||||
for k in ["name", "description", "version", "maintainer"]
|
|
||||||
):
|
|
||||||
print(f"Image config {yaml_path} missing required fields")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Use Image.model_validate to handle all fields from YAML
|
|
||||||
# This will map all fields according to the Image model structure
|
|
||||||
try:
|
|
||||||
# Ensure image field is set if not in YAML
|
|
||||||
if "image" not in image_data:
|
|
||||||
image_data["image"] = f"monadical/cubbi-{image_data['name']}:latest"
|
|
||||||
|
|
||||||
image = Image.model_validate(image_data)
|
|
||||||
return image
|
|
||||||
except Exception as validation_error:
|
|
||||||
print(
|
|
||||||
f"Error validating image data from {yaml_path}: {validation_error}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading image from {yaml_path}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _load_package_images(self) -> Dict[str, Image]:
|
|
||||||
"""Load all package images from the cubbi/images directory"""
|
|
||||||
images = {}
|
|
||||||
|
|
||||||
if not BUILTIN_IMAGES_DIR.exists():
|
|
||||||
return images
|
|
||||||
|
|
||||||
# Search for cubbi-image.yaml files in each subdirectory
|
|
||||||
for image_dir in BUILTIN_IMAGES_DIR.iterdir():
|
|
||||||
if image_dir.is_dir():
|
|
||||||
image = self.load_image_from_dir(image_dir)
|
|
||||||
if image:
|
|
||||||
images[image.name] = image
|
|
||||||
|
|
||||||
return images
|
|
||||||
|
|
||||||
def get_image_path(self, image_name: str) -> Optional[Path]:
|
|
||||||
"""Get the directory path for an image"""
|
|
||||||
# Check package images first (these are the bundled ones)
|
|
||||||
package_path = BUILTIN_IMAGES_DIR / image_name
|
|
||||||
if package_path.exists() and package_path.is_dir():
|
|
||||||
return package_path
|
|
||||||
|
|
||||||
# Then check user images
|
|
||||||
user_path = self.images_dir / image_name
|
|
||||||
if user_path.exists() and user_path.is_dir():
|
|
||||||
return user_path
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
MAI container image management
|
|
||||||
"""
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""
|
|
||||||
Base image implementation for MAI
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from ..models import Image
|
|
||||||
|
|
||||||
|
|
||||||
class ImageManager:
|
|
||||||
"""Manager for MAI images"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_default_images() -> Dict[str, Image]:
|
|
||||||
"""Get the default built-in images"""
|
|
||||||
from ..config import DEFAULT_IMAGES
|
|
||||||
|
|
||||||
return DEFAULT_IMAGES
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_image_metadata(image_name: str) -> Optional[Dict]:
|
|
||||||
"""Get metadata for a specific image"""
|
|
||||||
from ..config import DEFAULT_IMAGES
|
|
||||||
|
|
||||||
if image_name in DEFAULT_IMAGES:
|
|
||||||
return DEFAULT_IMAGES[image_name].model_dump()
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
# Cubbi - Container Tool
|
# MC - Monadical AI Container Tool
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Cubbi is a command-line tool for managing ephemeral
|
MC (Monadical Container) is a command-line tool for managing ephemeral
|
||||||
containers that run AI tools and development environments. It works with both
|
containers that run AI tools and development environments. It works with both
|
||||||
local Docker and a dedicated remote web service that manages containers in a
|
local Docker and a dedicated remote web service that manages containers in a
|
||||||
Docker-in-Docker (DinD) environment.
|
Docker-in-Docker (DinD) environment.
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
### Cubbi Service
|
### MC Service
|
||||||
- **Web Framework**: FastAPI for high-performance, async API endpoints
|
- **Web Framework**: FastAPI for high-performance, async API endpoints
|
||||||
- **Package Management**: uv (Astral) for dependency management
|
- **Package Management**: uv (Astral) for dependency management
|
||||||
- **Database**: SQLite for development, PostgreSQL for production
|
- **Database**: SQLite for development, PostgreSQL for production
|
||||||
- **Container Management**: Docker SDK for Python
|
- **Container Management**: Docker SDK for Python
|
||||||
- **Authentication**: OAuth 2.0 integration with Authentik
|
- **Authentication**: OAuth 2.0 integration with Authentik
|
||||||
|
|
||||||
### Cubbi CLI
|
### MC CLI
|
||||||
- **Language**: Python
|
- **Language**: Python
|
||||||
- **Package Management**: uv for dependency management
|
- **Package Management**: uv for dependency management
|
||||||
- **Distribution**: Standalone binary via PyInstaller or similar
|
- **Distribution**: Standalone binary via PyInstaller or similar
|
||||||
@@ -26,17 +26,17 @@ Docker-in-Docker (DinD) environment.
|
|||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
1. **CLI Tool (`cubbi`)**: The command-line interface users interact with
|
1. **CLI Tool (`mc`)**: The command-line interface users interact with
|
||||||
2. **Cubbi Service**: A web service that handles remote container execution
|
2. **MC Service**: A web service that handles remote container execution
|
||||||
3. **Container Images**: Predefined container templates for various AI tools
|
3. **Container Drivers**: Predefined container templates for various AI tools
|
||||||
|
|
||||||
### Architecture Diagram
|
### Architecture Diagram
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────────────────┐
|
┌─────────────┐ ┌─────────────────────────┐
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ Cubbi CLI │◄─────────►│ Local Docker Daemon │
|
│ MC CLI │◄─────────►│ Local Docker Daemon │
|
||||||
│ (cubbi) │ │ │
|
│ (mc) │ │ │
|
||||||
│ │ └─────────────────────────┘
|
│ │ └─────────────────────────┘
|
||||||
└──────┬──────┘
|
└──────┬──────┘
|
||||||
│
|
│
|
||||||
@@ -44,8 +44,8 @@ Docker-in-Docker (DinD) environment.
|
|||||||
│
|
│
|
||||||
┌──────▼──────┐ ┌─────────────────────────┐
|
┌──────▼──────┐ ┌─────────────────────────┐
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ Cubbi │◄─────────►│ Docker-in-Docker │
|
│ MC Service │◄─────────►│ Docker-in-Docker │
|
||||||
│ Service │ │ │
|
│ (Web API) │ │ │
|
||||||
│ │ └─────────────────────────┘
|
│ │ └─────────────────────────┘
|
||||||
└─────────────┘
|
└─────────────┘
|
||||||
│
|
│
|
||||||
@@ -61,23 +61,23 @@ Docker-in-Docker (DinD) environment.
|
|||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
- **Session**: An active container instance with a specific image
|
- **Session**: An active container instance with a specific driver
|
||||||
- **Image**: A predefined container template with specific AI tools installed
|
- **Driver**: A predefined container template with specific AI tools installed
|
||||||
- **Remote**: A configured cubbi service instance
|
- **Remote**: A configured MC service instance
|
||||||
|
|
||||||
## User Configuration
|
## User Configuration
|
||||||
|
|
||||||
Cubbi supports user-specific configuration via a YAML file located at `~/.config/cubbi/config.yaml`. This provides a way to set default values, store service credentials, and customize behavior without modifying code.
|
MC supports user-specific configuration via a YAML file located at `~/.config/mc/config.yaml`. This provides a way to set default values, store service credentials, and customize behavior without modifying code.
|
||||||
|
|
||||||
### Configuration File Structure
|
### Configuration File Structure
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ~/.config/cubbi/config.yaml
|
# ~/.config/mc/config.yaml
|
||||||
defaults:
|
defaults:
|
||||||
image: "goose" # Default image to use
|
driver: "goose" # Default driver to use
|
||||||
connect: true # Automatically connect after creating session
|
connect: true # Automatically connect after creating session
|
||||||
mount_local: true # Mount local directory by default
|
mount_local: true # Mount local directory by default
|
||||||
networks: [] # Default networks to connect to (besides cubbi-network)
|
networks: [] # Default networks to connect to (besides mc-network)
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Service credentials with simplified naming
|
# Service credentials with simplified naming
|
||||||
@@ -97,17 +97,17 @@ services:
|
|||||||
api_key: "sk-or-..."
|
api_key: "sk-or-..."
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
network: "cubbi-network" # Default Docker network to use
|
network: "mc-network" # Default Docker network to use
|
||||||
socket: "/var/run/docker.sock" # Docker socket path
|
socket: "/var/run/docker.sock" # Docker socket path
|
||||||
|
|
||||||
remote:
|
remote:
|
||||||
default: "production" # Default remote to use
|
default: "production" # Default remote to use
|
||||||
endpoints:
|
endpoints:
|
||||||
production:
|
production:
|
||||||
url: "https://cubbi.monadical.com"
|
url: "https://mc.monadical.com"
|
||||||
auth_method: "oauth"
|
auth_method: "oauth"
|
||||||
staging:
|
staging:
|
||||||
url: "https://cubbi-staging.monadical.com"
|
url: "https://mc-staging.monadical.com"
|
||||||
auth_method: "oauth"
|
auth_method: "oauth"
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
@@ -145,22 +145,22 @@ The simplified configuration names are mapped to environment variables:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View entire configuration
|
# View entire configuration
|
||||||
cubbi config list
|
mc config list
|
||||||
|
|
||||||
# Get specific configuration value
|
# Get specific configuration value
|
||||||
cubbi config get defaults.driver
|
mc config get defaults.driver
|
||||||
|
|
||||||
# Set configuration value (using simplified naming)
|
# Set configuration value (using simplified naming)
|
||||||
cubbi config set langfuse.url "https://cloud.langfuse.com"
|
mc config set langfuse.url "https://cloud.langfuse.com"
|
||||||
cubbi config set openai.api_key "sk-..."
|
mc config set openai.api_key "sk-..."
|
||||||
|
|
||||||
# Network configuration
|
# Network configuration
|
||||||
cubbi config network list # List default networks
|
mc config network list # List default networks
|
||||||
cubbi config network add example-network # Add a network to defaults
|
mc config network add example-network # Add a network to defaults
|
||||||
cubbi config network remove example-network # Remove a network from defaults
|
mc config network remove example-network # Remove a network from defaults
|
||||||
|
|
||||||
# Reset configuration to defaults
|
# Reset configuration to defaults
|
||||||
cubbi config reset
|
mc config reset
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Tool Commands
|
## CLI Tool Commands
|
||||||
@@ -169,81 +169,81 @@ cubbi config reset
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new session locally (shorthand)
|
# Create a new session locally (shorthand)
|
||||||
cubbi
|
mc
|
||||||
|
|
||||||
# List active sessions on local system
|
# List active sessions on local system
|
||||||
cubbi session list
|
mc session list
|
||||||
|
|
||||||
# Create a new session locally
|
# Create a new session locally
|
||||||
cubbi session create [OPTIONS]
|
mc session create [OPTIONS]
|
||||||
|
|
||||||
# Create a session with a specific image
|
# Create a session with a specific driver
|
||||||
cubbi session create --image goose
|
mc session create --driver goose
|
||||||
|
|
||||||
# Create a session with a specific project repository
|
# Create a session with a specific project repository
|
||||||
cubbi session create --image goose --project github.com/hello/private
|
mc session create --driver goose --project github.com/hello/private
|
||||||
|
|
||||||
# Create a session with external networks
|
# Create a session with external networks
|
||||||
cubbi session create --network teamnet --network othernetwork
|
mc session create --network teamnet --network othernetwork
|
||||||
|
|
||||||
# Create a session with a project (shorthand)
|
# Create a session with a project (shorthand)
|
||||||
cubbi git@github.com:hello/private
|
mc git@github.com:hello/private
|
||||||
|
|
||||||
# Close a specific session
|
# Close a specific session
|
||||||
cubbi session close <id>
|
mc session close <id>
|
||||||
|
|
||||||
# Connect to an existing session
|
# Connect to an existing session
|
||||||
cubbi session connect <id>
|
mc session connect <id>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remote Management
|
### Remote Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add a remote Cubbi service
|
# Add a remote MC service
|
||||||
cubbi remote add <name> <url>
|
mc remote add <name> <url>
|
||||||
|
|
||||||
# List configured remote services
|
# List configured remote services
|
||||||
cubbi remote list
|
mc remote list
|
||||||
|
|
||||||
# Remove a remote service
|
# Remove a remote service
|
||||||
cubbi remote remove <name>
|
mc remote remove <name>
|
||||||
|
|
||||||
# Authenticate with a remote service
|
# Authenticate with a remote service
|
||||||
cubbi -r <remote_name> auth
|
mc -r <remote_name> auth
|
||||||
|
|
||||||
# Create a session on a remote service
|
# Create a session on a remote service
|
||||||
cubbi -r <remote_name> [session create]
|
mc -r <remote_name> [session create]
|
||||||
|
|
||||||
# List sessions on a remote service
|
# List sessions on a remote service
|
||||||
cubbi -r <remote_name> session list
|
mc -r <remote_name> session list
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set environment variables for a session
|
# Set environment variables for a session
|
||||||
cubbi session create -e VAR1=value1 -e VAR2=value2
|
mc session create -e VAR1=value1 -e VAR2=value2
|
||||||
|
|
||||||
# Set environment variables for a remote session
|
# Set environment variables for a remote session
|
||||||
cubbi -r <remote_name> session create -e VAR1=value1
|
mc -r <remote_name> session create -e VAR1=value1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stream logs from a session
|
# Stream logs from a session
|
||||||
cubbi session logs <id>
|
mc session logs <id>
|
||||||
|
|
||||||
# Stream logs with follow option
|
# Stream logs with follow option
|
||||||
cubbi session logs <id> -f
|
mc session logs <id> -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cubbi Service Specification
|
## MC Service Specification
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
The Cubbi Service is a web service that manages ephemeral containers in a Docker-in-Docker environment. It provides a REST API for container lifecycle management, authentication, and real-time log streaming.
|
The MC Service is a web service that manages ephemeral containers in a Docker-in-Docker environment. It provides a REST API for container lifecycle management, authentication, and real-time log streaming.
|
||||||
|
|
||||||
### API Endpoints
|
### API Endpoints
|
||||||
|
|
||||||
@@ -258,19 +258,19 @@ POST /auth/logout - Invalidate current token
|
|||||||
|
|
||||||
### Authentik Integration
|
### Authentik Integration
|
||||||
|
|
||||||
The Cubbi Service integrates with Authentik at https://authentik.monadical.io using OAuth 2.0:
|
The MC Service integrates with Authentik at https://authentik.monadical.io using OAuth 2.0:
|
||||||
|
|
||||||
1. **Application Registration**:
|
1. **Application Registration**:
|
||||||
- Cubbi Service is registered as an OAuth application in Authentik
|
- MC Service is registered as an OAuth application in Authentik
|
||||||
- Configured with redirect URI to `/auth/callback`
|
- Configured with redirect URI to `/auth/callback`
|
||||||
- Assigned appropriate scopes for user identification
|
- Assigned appropriate scopes for user identification
|
||||||
|
|
||||||
2. **Authentication Flow**:
|
2. **Authentication Flow**:
|
||||||
- User initiates authentication via CLI
|
- User initiates authentication via CLI
|
||||||
- Cubbi CLI opens browser to Authentik authorization URL
|
- MC CLI opens browser to Authentik authorization URL
|
||||||
- User logs in through Authentik's interface
|
- User logs in through Authentik's interface
|
||||||
- Authentik redirects to callback URL with authorization code
|
- Authentik redirects to callback URL with authorization code
|
||||||
- Cubbi Service exchanges code for access and refresh tokens
|
- MC Service exchanges code for access and refresh tokens
|
||||||
- CLI receives and securely stores tokens
|
- CLI receives and securely stores tokens
|
||||||
|
|
||||||
3. **Token Management**:
|
3. **Token Management**:
|
||||||
@@ -289,11 +289,11 @@ POST /sessions/{id}/connect - Establish connection to session
|
|||||||
GET /sessions/{id}/logs - Stream session logs
|
GET /sessions/{id}/logs - Stream session logs
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Images
|
#### Drivers
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /images - List available images
|
GET /drivers - List available drivers
|
||||||
GET /images/{name} - Get image details
|
GET /drivers/{name} - Get driver details
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Projects
|
#### Projects
|
||||||
@@ -309,19 +309,19 @@ DELETE /projects/{id} - Remove a project
|
|||||||
### Service Configuration
|
### Service Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# cubbi-service.yaml
|
# mc-service.yaml
|
||||||
server:
|
server:
|
||||||
port: 3000
|
port: 3000
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
socket: /var/run/docker.sock
|
socket: /var/run/docker.sock
|
||||||
network: cubbi-network
|
network: mc-network
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
provider: authentik
|
provider: authentik
|
||||||
url: https://authentik.monadical.io
|
url: https://authentik.monadical.io
|
||||||
clientId: cubbi-service
|
clientId: mc-service
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
providers:
|
providers:
|
||||||
@@ -332,13 +332,13 @@ logging:
|
|||||||
public_key: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
public_key: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
||||||
secret_key: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
secret_key: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
||||||
|
|
||||||
images:
|
drivers:
|
||||||
- name: goose
|
- name: goose
|
||||||
image: monadical/cubbi-goose:latest
|
image: monadical/mc-goose:latest
|
||||||
- name: aider
|
- name: aider
|
||||||
image: monadical/cubbi-aider:latest
|
image: monadical/mc-aider:latest
|
||||||
- name: claude-code
|
- name: claude-code
|
||||||
image: monadical/cubbi-claude-code:latest
|
image: monadical/mc-claude-code:latest
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
storage:
|
storage:
|
||||||
@@ -352,7 +352,7 @@ projects:
|
|||||||
|
|
||||||
### Docker-in-Docker Implementation
|
### Docker-in-Docker Implementation
|
||||||
|
|
||||||
The Cubbi Service runs in a container with access to the host's Docker socket, allowing it to create and manage sibling containers. This approach provides:
|
The MC Service runs in a container with access to the host's Docker socket, allowing it to create and manage sibling containers. This approach provides:
|
||||||
|
|
||||||
1. Isolation between containers
|
1. Isolation between containers
|
||||||
2. Simple lifecycle management
|
2. Simple lifecycle management
|
||||||
@@ -367,7 +367,7 @@ For remote connections to containers, the service provides two methods:
|
|||||||
|
|
||||||
### Logging Implementation
|
### Logging Implementation
|
||||||
|
|
||||||
The Cubbi Service implements log collection and forwarding:
|
The MC Service implements log collection and forwarding:
|
||||||
|
|
||||||
1. Container logs are captured using Docker's logging drivers
|
1. Container logs are captured using Docker's logging drivers
|
||||||
2. Logs are forwarded to configured providers (Fluentd, Langfuse)
|
2. Logs are forwarded to configured providers (Fluentd, Langfuse)
|
||||||
@@ -377,22 +377,22 @@ The Cubbi Service implements log collection and forwarding:
|
|||||||
|
|
||||||
### Persistent Project Configuration
|
### Persistent Project Configuration
|
||||||
|
|
||||||
Cubbi provides persistent storage for project-specific configurations that need to survive container restarts. This is implemented through a dedicated volume mount and symlink system:
|
MC provides persistent storage for project-specific configurations that need to survive container restarts. This is implemented through a dedicated volume mount and symlink system:
|
||||||
|
|
||||||
1. **Configuration Storage**:
|
1. **Configuration Storage**:
|
||||||
- Each project has a dedicated configuration directory on the host at `~/.cubbi/projects/<project-hash>/config`
|
- Each project has a dedicated configuration directory on the host at `~/.mc/projects/<project-hash>/config`
|
||||||
- For projects specified by URL, the hash is derived from the repository URL
|
- For projects specified by URL, the hash is derived from the repository URL
|
||||||
- For local projects, the hash is derived from the absolute path of the local directory
|
- For local projects, the hash is derived from the absolute path of the local directory
|
||||||
- This directory is mounted into the container at `/cubbi-config`
|
- This directory is mounted into the container at `/mc-config`
|
||||||
|
|
||||||
2. **Image Configuration**:
|
2. **Driver Configuration**:
|
||||||
- Each image can specify configuration files/directories that should persist across sessions
|
- Each driver can specify configuration files/directories that should persist across sessions
|
||||||
- These are defined in the image's `cubbi-image.yaml` file in the `persistent_configs` section
|
- These are defined in the driver's `mc-driver.yaml` file in the `persistent_configs` section
|
||||||
- Example for Goose image:
|
- Example for Goose driver:
|
||||||
```yaml
|
```yaml
|
||||||
persistent_configs:
|
persistent_configs:
|
||||||
- source: "/app/.goose" # Path in container
|
- source: "/app/.goose" # Path in container
|
||||||
target: "/cubbi-config/goose" # Path in persistent storage
|
target: "/mc-config/goose" # Path in persistent storage
|
||||||
type: "directory" # directory or file
|
type: "directory" # directory or file
|
||||||
description: "Goose memory and configuration"
|
description: "Goose memory and configuration"
|
||||||
```
|
```
|
||||||
@@ -406,8 +406,8 @@ Cubbi provides persistent storage for project-specific configurations that need
|
|||||||
4. **Environment Variables**:
|
4. **Environment Variables**:
|
||||||
- Container has access to configuration location via environment variables:
|
- Container has access to configuration location via environment variables:
|
||||||
```
|
```
|
||||||
CUBBI_CONFIG_DIR=/cubbi-config
|
MC_CONFIG_DIR=/mc-config
|
||||||
CUBBI_IMAGE_CONFIG_DIR=/cubbi-config/<image-name>
|
MC_DRIVER_CONFIG_DIR=/mc-config/<driver-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures that important configurations like Goose's memory store, authentication tokens, and other state information persist between container sessions while maintaining isolation between different projects.
|
This ensures that important configurations like Goose's memory store, authentication tokens, and other state information persist between container sessions while maintaining isolation between different projects.
|
||||||
@@ -418,21 +418,21 @@ Users can add projects with associated credentials:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add a project with SSH key
|
# Add a project with SSH key
|
||||||
cubbi project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
|
mc project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
|
||||||
|
|
||||||
# Add a project with token authentication
|
# Add a project with token authentication
|
||||||
cubbi project add github.com/hello/private --token ghp_123456789
|
mc project add github.com/hello/private --token ghp_123456789
|
||||||
|
|
||||||
# List all projects
|
# List all projects
|
||||||
cubbi project list
|
mc project list
|
||||||
|
|
||||||
# Remove a project
|
# Remove a project
|
||||||
cubbi project remove github.com/hello/private
|
mc project remove github.com/hello/private
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Configuration
|
### Project Configuration
|
||||||
|
|
||||||
Projects are stored in the Cubbi service and referenced by their repository URL. The configuration includes:
|
Projects are stored in the MC service and referenced by their repository URL. The configuration includes:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Project configuration
|
# Project configuration
|
||||||
@@ -448,59 +448,60 @@ auth:
|
|||||||
public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
|
public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Image Implementation
|
## Driver Implementation
|
||||||
|
|
||||||
### Image Structure
|
### Driver Structure
|
||||||
|
|
||||||
Each image is a Docker container with a standardized structure:
|
Each driver is a Docker image with a standardized structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
/
|
/
|
||||||
├── entrypoint.sh # Container initialization
|
├── entrypoint.sh # Container initialization
|
||||||
├── cubbi-init.sh # Standardized initialization script
|
├── mc-init.sh # Standardized initialization script
|
||||||
├── cubbi-image.yaml # Image metadata and configuration
|
├── mc-driver.yaml # Driver metadata and configuration
|
||||||
├── tool/ # AI tool installation
|
├── tool/ # AI tool installation
|
||||||
└── ssh/ # SSH server configuration
|
└── ssh/ # SSH server configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standardized Initialization Script
|
### Standardized Initialization Script
|
||||||
|
|
||||||
All images include a standardized `cubbi-init.sh` script that handles common initialization tasks:
|
All drivers include a standardized `mc-init.sh` script that handles common initialization tasks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Project initialization
|
# Project initialization
|
||||||
if [ -n "$CUBBI_PROJECT_URL" ]; then
|
if [ -n "$MC_PROJECT_URL" ]; then
|
||||||
echo "Initializing project: $CUBBI_PROJECT_URL"
|
echo "Initializing project: $MC_PROJECT_URL"
|
||||||
|
|
||||||
# Set up SSH key if provided
|
# Set up SSH key if provided
|
||||||
if [ -n "$CUBBI_GIT_SSH_KEY" ]; then
|
if [ -n "$MC_GIT_SSH_KEY" ]; then
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "$CUBBI_GIT_SSH_KEY" > ~/.ssh/id_ed25519
|
echo "$MC_GIT_SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up token if provided
|
# Set up token if provided
|
||||||
if [ -n "$CUBBI_GIT_TOKEN" ]; then
|
if [ -n "$MC_GIT_TOKEN" ]; then
|
||||||
git config --global credential.helper store
|
git config --global credential.helper store
|
||||||
echo "https://$CUBBI_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
|
echo "https://$MC_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clone repository
|
# Clone repository
|
||||||
git clone $CUBBI_PROJECT_URL /app
|
git clone $MC_PROJECT_URL /app
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
# Run project-specific initialization if present
|
# Run project-specific initialization if present
|
||||||
if [ -f "/app/.cubbi/init.sh" ]; then
|
if [ -f "/app/.mc/init.sh" ]; then
|
||||||
bash /app/.cubbi/init.sh
|
bash /app/.mc/init.sh
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Image-specific initialization continues...
|
# Driver-specific initialization continues...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Image Configuration (cubbi-image.yaml)
|
### Driver Configuration (mc-driver.yaml)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: goose
|
name: goose
|
||||||
@@ -509,7 +510,7 @@ version: 1.0.0
|
|||||||
maintainer: team@monadical.com
|
maintainer: team@monadical.com
|
||||||
|
|
||||||
init:
|
init:
|
||||||
pre_command: /cubbi-init.sh
|
pre_command: /mc-init.sh
|
||||||
command: /entrypoint.sh
|
command: /entrypoint.sh
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -523,21 +524,21 @@ environment:
|
|||||||
required: false
|
required: false
|
||||||
|
|
||||||
# Project environment variables
|
# Project environment variables
|
||||||
- name: CUBBI_PROJECT_URL
|
- name: MC_PROJECT_URL
|
||||||
description: Project repository URL
|
description: Project repository URL
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- name: CUBBI_PROJECT_TYPE
|
- name: MC_PROJECT_TYPE
|
||||||
description: Project repository type (git, svn, etc.)
|
description: Project repository type (git, svn, etc.)
|
||||||
required: false
|
required: false
|
||||||
default: git
|
default: git
|
||||||
|
|
||||||
- name: CUBBI_GIT_SSH_KEY
|
- name: MC_GIT_SSH_KEY
|
||||||
description: SSH key for Git authentication
|
description: SSH key for Git authentication
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
|
|
||||||
- name: CUBBI_GIT_TOKEN
|
- name: MC_GIT_TOKEN
|
||||||
description: Token for Git authentication
|
description: Token for Git authentication
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
@@ -552,12 +553,12 @@ volumes:
|
|||||||
|
|
||||||
persistent_configs:
|
persistent_configs:
|
||||||
- source: "/app/.goose"
|
- source: "/app/.goose"
|
||||||
target: "/cubbi-config/goose"
|
target: "/mc-config/goose"
|
||||||
type: "directory"
|
type: "directory"
|
||||||
description: "Goose memory and configuration"
|
description: "Goose memory and configuration"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Built-in images
|
### Example Built-in Drivers
|
||||||
|
|
||||||
1. **goose**: Goose with MCP servers
|
1. **goose**: Goose with MCP servers
|
||||||
2. **aider**: Aider coding assistant
|
2. **aider**: Aider coding assistant
|
||||||
@@ -568,32 +569,32 @@ persistent_configs:
|
|||||||
|
|
||||||
### Docker Network Integration
|
### Docker Network Integration
|
||||||
|
|
||||||
Cubbi provides flexible network management for containers:
|
MC provides flexible network management for containers:
|
||||||
|
|
||||||
1. **Default Cubbi Network**:
|
1. **Default MC Network**:
|
||||||
- Each container is automatically connected to the Cubbi network (`cubbi-network` by default)
|
- Each container is automatically connected to the MC network (`mc-network` by default)
|
||||||
- This ensures containers can communicate with each other
|
- This ensures containers can communicate with each other
|
||||||
|
|
||||||
2. **External Network Connection**:
|
2. **External Network Connection**:
|
||||||
- Containers can be connected to one or more external Docker networks
|
- Containers can be connected to one or more external Docker networks
|
||||||
- This allows integration with existing infrastructure (e.g., databases, web servers)
|
- This allows integration with existing infrastructure (e.g., databases, web servers)
|
||||||
- Networks can be specified at session creation time: `cubbi session create --network mynetwork`
|
- Networks can be specified at session creation time: `mc session create --network mynetwork`
|
||||||
|
|
||||||
3. **Default Networks Configuration**:
|
3. **Default Networks Configuration**:
|
||||||
- Users can configure default networks in their configuration
|
- Users can configure default networks in their configuration
|
||||||
- These networks will be used for all new sessions unless overridden
|
- These networks will be used for all new sessions unless overridden
|
||||||
- Managed with `cubbi config network` commands
|
- Managed with `mc config network` commands
|
||||||
|
|
||||||
4. **Network Command Examples**:
|
4. **Network Command Examples**:
|
||||||
```bash
|
```bash
|
||||||
# Use with session creation
|
# Use with session creation
|
||||||
cubbi session create --network teamnet
|
mc session create --network teamnet
|
||||||
|
|
||||||
# Use with multiple networks
|
# Use with multiple networks
|
||||||
cubbi session create --network teamnet --network dbnet
|
mc session create --network teamnet --network dbnet
|
||||||
|
|
||||||
# Configure default networks
|
# Configure default networks
|
||||||
cubbi config network add teamnet
|
mc config network add teamnet
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
@@ -606,15 +607,15 @@ Cubbi provides flexible network management for containers:
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Cubbi Service Deployment
|
### MC Service Deployment
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml for Cubbi Service
|
# docker-compose.yml for MC Service
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cubbi-service:
|
mc-service:
|
||||||
image: monadical/cubbi-service:latest
|
image: monadical/mc-service:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
@@ -624,10 +625,10 @@ services:
|
|||||||
- AUTH_URL=https://authentik.monadical.io
|
- AUTH_URL=https://authentik.monadical.io
|
||||||
- LANGFUSE_API_KEY=your_api_key
|
- LANGFUSE_API_KEY=your_api_key
|
||||||
networks:
|
networks:
|
||||||
- cubbi-network
|
- mc-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cubbi-network:
|
mc-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -637,33 +638,33 @@ networks:
|
|||||||
|
|
||||||
1. User adds project repository with authentication:
|
1. User adds project repository with authentication:
|
||||||
```bash
|
```bash
|
||||||
cubbi project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
|
mc project add github.com/hello/private --ssh-key ~/.ssh/id_ed25519
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Cubbi CLI reads the SSH key, encrypts it, and sends to Cubbi Service
|
2. MC CLI reads the SSH key, encrypts it, and sends to MC Service
|
||||||
|
|
||||||
3. Cubbi Service stores the project configuration securely
|
3. MC Service stores the project configuration securely
|
||||||
|
|
||||||
### Using a Project in a Session
|
### Using a Project in a Session
|
||||||
|
|
||||||
1. User creates a session with a project:
|
1. User creates a session with a project:
|
||||||
```bash
|
```bash
|
||||||
cubbi -r monadical git@github.com:hello/private
|
mc -r monadical git@github.com:hello/private
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Cubbi Service:
|
2. MC Service:
|
||||||
- Identifies the project from the URL
|
- Identifies the project from the URL
|
||||||
- Retrieves project authentication details
|
- Retrieves project authentication details
|
||||||
- Sets up environment variables:
|
- Sets up environment variables:
|
||||||
```
|
```
|
||||||
CUBBI_PROJECT_URL=git@github.com:hello/private
|
MC_PROJECT_URL=git@github.com:hello/private
|
||||||
CUBBI_PROJECT_TYPE=git
|
MC_PROJECT_TYPE=git
|
||||||
CUBBI_GIT_SSH_KEY=<contents of the SSH key>
|
MC_GIT_SSH_KEY=<contents of the SSH key>
|
||||||
```
|
```
|
||||||
- Creates container with these environment variables
|
- Creates container with these environment variables
|
||||||
|
|
||||||
3. Container initialization:
|
3. Container initialization:
|
||||||
- The standardized `cubbi-init.sh` script detects the project environment variables
|
- The standardized `mc-init.sh` script detects the project environment variables
|
||||||
- Sets up SSH key or token authentication
|
- Sets up SSH key or token authentication
|
||||||
- Clones the repository to `/app`
|
- Clones the repository to `/app`
|
||||||
- Runs any project-specific initialization scripts
|
- Runs any project-specific initialization scripts
|
||||||
@@ -673,10 +674,10 @@ networks:
|
|||||||
## Implementation Roadmap
|
## Implementation Roadmap
|
||||||
|
|
||||||
1. **Phase 1**: Local CLI tool with Docker integration
|
1. **Phase 1**: Local CLI tool with Docker integration
|
||||||
2. **Phase 2**: Cubbi Service REST API with basic container management
|
2. **Phase 2**: MC Service REST API with basic container management
|
||||||
3. **Phase 3**: Authentication and secure connections
|
3. **Phase 3**: Authentication and secure connections
|
||||||
4. **Phase 4**: Project management functionality
|
4. **Phase 4**: Project management functionality
|
||||||
5. **Phase 5**: Image implementation (Goose, Aider, Claude Code)
|
5. **Phase 5**: Driver implementation (Goose, Aider, Claude Code)
|
||||||
6. **Phase 6**: Logging integration with Fluentd and Langfuse
|
6. **Phase 6**: Logging integration with Fluentd and Langfuse
|
||||||
7. **Phase 7**: CLI remote connectivity improvements
|
7. **Phase 7**: CLI remote connectivity improvements
|
||||||
8. **Phase 8**: Additional images and extensibility features
|
8. **Phase 8**: Additional drivers and extensibility features
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document specifies the implementation for Model Control Protocol (MCP) server support in the Cubbi system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to Cubbi sessions.
|
This document specifies the implementation for Model Control Protocol (MCP) server support in the Monadical Container (MC) system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to MC sessions.
|
||||||
|
|
||||||
An MCP server is a service that can be accessed by a image (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
|
An MCP server is a service that can be accessed by a driver (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
|
||||||
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
|
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
|
||||||
- A remote HTTP SSE server accessed directly via its URL
|
- A remote HTTP SSE server accessed directly via its URL
|
||||||
|
|
||||||
@@ -53,48 +53,48 @@ mcps:
|
|||||||
### MCP Management
|
### MCP Management
|
||||||
|
|
||||||
```
|
```
|
||||||
cubbi mcp list # List all configured MCP servers and their status
|
mc mcp list # List all configured MCP servers and their status
|
||||||
cubbi mcp status <name> # Show detailed status of a specific MCP server
|
mc mcp status <name> # Show detailed status of a specific MCP server
|
||||||
cubbi mcp start <name> # Start an MCP server container
|
mc mcp start <name> # Start an MCP server container
|
||||||
cubbi mcp stop <name> # Stop and remove an MCP server container
|
mc mcp stop <name> # Stop and remove an MCP server container
|
||||||
cubbi mcp restart <name> # Restart an MCP server container
|
mc mcp restart <name> # Restart an MCP server container
|
||||||
cubbi mcp start --all # Start all MCP server containers
|
mc mcp start --all # Start all MCP server containers
|
||||||
cubbi mcp stop --all # Stop and remove all MCP server containers
|
mc mcp stop --all # Stop and remove all MCP server containers
|
||||||
cubbi mcp inspector # Run the MCP Inspector UI with network connectivity to all MCP servers
|
mc mcp inspector # Run the MCP Inspector UI with network connectivity to all MCP servers
|
||||||
cubbi mcp inspector --client-port <cp> --server-port <sp> # Run with custom client port (default: 5173) and server port (default: 3000)
|
mc mcp inspector --client-port <cp> --server-port <sp> # Run with custom client port (default: 5173) and server port (default: 3000)
|
||||||
cubbi mcp inspector --detach # Run the inspector in detached mode
|
mc mcp inspector --detach # Run the inspector in detached mode
|
||||||
cubbi mcp inspector --stop # Stop the running inspector
|
mc mcp inspector --stop # Stop the running inspector
|
||||||
cubbi mcp logs <name> # Show logs for an MCP server container
|
mc mcp logs <name> # Show logs for an MCP server container
|
||||||
```
|
```
|
||||||
|
|
||||||
### MCP Configuration
|
### MCP Configuration
|
||||||
|
|
||||||
```
|
```
|
||||||
# Add a proxy-based MCP server (default)
|
# Add a proxy-based MCP server (default)
|
||||||
cubbi mcp add <name> <base_image> [--command CMD] [--proxy-image IMG] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...]
|
mc mcp add <name> <base_image> [--command CMD] [--proxy-image IMG] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...]
|
||||||
|
|
||||||
# Add a remote MCP server
|
# Add a remote MCP server
|
||||||
cubbi mcp add-remote <name> <url> [--header KEY=VALUE...]
|
mc mcp add-remote <name> <url> [--header KEY=VALUE...]
|
||||||
|
|
||||||
# Remove an MCP configuration
|
# Remove an MCP configuration
|
||||||
cubbi mcp remove <name>
|
mc mcp remove <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Session Integration
|
### Session Integration
|
||||||
|
|
||||||
```
|
```
|
||||||
cubbi session create [--mcp <name>] # Create a session with an MCP server attached
|
mc session create [--mcp <name>] # Create a session with an MCP server attached
|
||||||
```
|
```
|
||||||
|
|
||||||
## Implementation Details
|
## Implementation Details
|
||||||
|
|
||||||
### MCP Container Management
|
### MCP Container Management
|
||||||
|
|
||||||
1. MCP containers will have their own dedicated Docker network (`cubbi-mcp-network`)
|
1. MCP containers will have their own dedicated Docker network (`mc-mcp-network`)
|
||||||
2. Session containers will be attached to both their session network and the MCP network when using an MCP
|
2. Session containers will be attached to both their session network and the MCP network when using an MCP
|
||||||
3. MCP containers will be persistent across sessions unless explicitly stopped
|
3. MCP containers will be persistent across sessions unless explicitly stopped
|
||||||
4. MCP containers will be named with a prefix to identify them (`cubbi_mcp_<name>`)
|
4. MCP containers will be named with a prefix to identify them (`mc_mcp_<name>`)
|
||||||
5. Each MCP container will have a network alias matching its name without the prefix (e.g., `cubbi_mcp_github` will have the alias `github`)
|
5. Each MCP container will have a network alias matching its name without the prefix (e.g., `mc_mcp_github` will have the alias `github`)
|
||||||
6. Network aliases enable DNS-based service discovery between containers
|
6. Network aliases enable DNS-based service discovery between containers
|
||||||
|
|
||||||
### MCP Inspector
|
### MCP Inspector
|
||||||
@@ -157,4 +157,4 @@ When a session is created with an MCP server:
|
|||||||
1. Support for MCP server version management
|
1. Support for MCP server version management
|
||||||
2. Health checking and automatic restart capabilities
|
2. Health checking and automatic restart capabilities
|
||||||
3. Support for MCP server clusters or load balancing
|
3. Support for MCP server clusters or load balancing
|
||||||
4. Integration with monitoring systems
|
4. Integration with monitoring systems
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Cubbi - Cubbi Container Tool
|
MC - Monadical Container Tool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
CLI for Cubbi Container Tool.
|
CLI for Monadical Container Tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from .config import ConfigManager
|
from .config import ConfigManager
|
||||||
from .container import ContainerManager
|
from .container import ContainerManager
|
||||||
from .mcp import MCPManager
|
|
||||||
from .models import SessionStatus
|
from .models import SessionStatus
|
||||||
from .session import SessionManager
|
|
||||||
from .user_config import UserConfigManager
|
from .user_config import UserConfigManager
|
||||||
|
from .session import SessionManager
|
||||||
|
from .mcp import MCPManager
|
||||||
|
|
||||||
# Configure logging - will only show logs if --verbose flag is used
|
# Configure logging - will only show logs if --verbose flag is used
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -24,13 +23,13 @@ logging.basicConfig(
|
|||||||
handlers=[logging.StreamHandler()],
|
handlers=[logging.StreamHandler()],
|
||||||
)
|
)
|
||||||
|
|
||||||
app = typer.Typer(help="Cubbi Container Tool", no_args_is_help=True)
|
app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True)
|
||||||
session_app = typer.Typer(help="Manage Cubbi sessions", no_args_is_help=True)
|
session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True)
|
||||||
image_app = typer.Typer(help="Manage Cubbi images", no_args_is_help=True)
|
driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True)
|
||||||
config_app = typer.Typer(help="Manage Cubbi configuration", no_args_is_help=True)
|
config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True)
|
||||||
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
|
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
|
||||||
app.add_typer(session_app, name="session", no_args_is_help=True)
|
app.add_typer(session_app, name="session", no_args_is_help=True)
|
||||||
app.add_typer(image_app, name="image", no_args_is_help=True)
|
app.add_typer(driver_app, name="driver", no_args_is_help=True)
|
||||||
app.add_typer(config_app, name="config", no_args_is_help=True)
|
app.add_typer(config_app, name="config", no_args_is_help=True)
|
||||||
app.add_typer(mcp_app, name="mcp", no_args_is_help=True)
|
app.add_typer(mcp_app, name="mcp", no_args_is_help=True)
|
||||||
|
|
||||||
@@ -49,10 +48,10 @@ def main(
|
|||||||
False, "--verbose", "-v", help="Enable verbose logging"
|
False, "--verbose", "-v", help="Enable verbose logging"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Cubbi Container Tool
|
"""Monadical Container Tool
|
||||||
|
|
||||||
Run 'cubbi session create' to create a new session.
|
Run 'mc session create' to create a new session.
|
||||||
Use 'cubbix' as a shortcut for 'cubbi session create'.
|
Use 'mcx' as a shortcut for 'mc session create'.
|
||||||
"""
|
"""
|
||||||
# Set log level based on verbose flag
|
# Set log level based on verbose flag
|
||||||
if verbose:
|
if verbose:
|
||||||
@@ -61,19 +60,19 @@ def main(
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def version() -> None:
|
def version() -> None:
|
||||||
"""Show Cubbi version information"""
|
"""Show MC version information"""
|
||||||
from importlib.metadata import version as get_version
|
from importlib.metadata import version as get_version
|
||||||
|
|
||||||
try:
|
try:
|
||||||
version_str = get_version("cubbi")
|
version_str = get_version("mcontainer")
|
||||||
console.print(f"Cubbi - Cubbi Container Tool v{version_str}")
|
console.print(f"MC - Monadical Container Tool v{version_str}")
|
||||||
except Exception:
|
except Exception:
|
||||||
console.print("Cubbi - Cubbi Container Tool (development version)")
|
console.print("MC - Monadical Container Tool (development version)")
|
||||||
|
|
||||||
|
|
||||||
@session_app.command("list")
|
@session_app.command("list")
|
||||||
def list_sessions() -> None:
|
def list_sessions() -> None:
|
||||||
"""List active Cubbi sessions"""
|
"""List active MC sessions"""
|
||||||
sessions = container_manager.list_sessions()
|
sessions = container_manager.list_sessions()
|
||||||
|
|
||||||
if not sessions:
|
if not sessions:
|
||||||
@@ -83,7 +82,7 @@ def list_sessions() -> None:
|
|||||||
table = Table(show_header=True, header_style="bold")
|
table = Table(show_header=True, header_style="bold")
|
||||||
table.add_column("ID")
|
table.add_column("ID")
|
||||||
table.add_column("Name")
|
table.add_column("Name")
|
||||||
table.add_column("Image")
|
table.add_column("Driver")
|
||||||
table.add_column("Status")
|
table.add_column("Status")
|
||||||
table.add_column("Ports")
|
table.add_column("Ports")
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ def list_sessions() -> None:
|
|||||||
table.add_row(
|
table.add_row(
|
||||||
session.id,
|
session.id,
|
||||||
session.name,
|
session.name,
|
||||||
session.image,
|
session.driver,
|
||||||
f"[{status_color}]{status_name}[/{status_color}]",
|
f"[{status_color}]{status_name}[/{status_color}]",
|
||||||
ports_str,
|
ports_str,
|
||||||
)
|
)
|
||||||
@@ -121,7 +120,7 @@ def list_sessions() -> None:
|
|||||||
|
|
||||||
@session_app.command("create")
|
@session_app.command("create")
|
||||||
def create_session(
|
def create_session(
|
||||||
image: Optional[str] = typer.Option(None, "--image", "-i", help="Image to use"),
|
driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"),
|
||||||
path_or_url: Optional[str] = typer.Argument(
|
path_or_url: Optional[str] = typer.Argument(
|
||||||
None,
|
None,
|
||||||
help="Local directory path to mount or repository URL to clone",
|
help="Local directory path to mount or repository URL to clone",
|
||||||
@@ -168,7 +167,7 @@ def create_session(
|
|||||||
),
|
),
|
||||||
ssh: bool = typer.Option(False, "--ssh", help="Start SSH server in the container"),
|
ssh: bool = typer.Option(False, "--ssh", help="Start SSH server in the container"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new Cubbi session
|
"""Create a new MC session
|
||||||
|
|
||||||
If a local directory path is provided, it will be mounted at /app in the container.
|
If a local directory path is provided, it will be mounted at /app in the container.
|
||||||
If a repository URL is provided, it will be cloned into /app during initialization.
|
If a repository URL is provided, it will be cloned into /app during initialization.
|
||||||
@@ -182,13 +181,11 @@ def create_session(
|
|||||||
target_gid = gid if gid is not None else os.getgid()
|
target_gid = gid if gid is not None else os.getgid()
|
||||||
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
|
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
|
||||||
|
|
||||||
# Use default image from user configuration
|
# Use default driver from user configuration
|
||||||
if not image:
|
if not driver:
|
||||||
image_name = user_config.get(
|
driver = user_config.get(
|
||||||
"defaults.image", config_manager.config.defaults.get("image", "goose")
|
"defaults.driver", config_manager.config.defaults.get("driver", "goose")
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
image_name = image
|
|
||||||
|
|
||||||
# Start with environment variables from user configuration
|
# Start with environment variables from user configuration
|
||||||
environment = user_config.get_environment_variables()
|
environment = user_config.get_environment_variables()
|
||||||
@@ -257,7 +254,7 @@ def create_session(
|
|||||||
for host_path, mount_info in volume_mounts.items():
|
for host_path, mount_info in volume_mounts.items():
|
||||||
console.print(f" {host_path} -> {mount_info['bind']}")
|
console.print(f" {host_path} -> {mount_info['bind']}")
|
||||||
|
|
||||||
with console.status(f"Creating session with image '{image_name}'..."):
|
with console.status(f"Creating session with driver '{driver}'..."):
|
||||||
# If path_or_url is a local directory, we should mount it
|
# If path_or_url is a local directory, we should mount it
|
||||||
# If it's a Git URL or doesn't exist, handle accordingly
|
# If it's a Git URL or doesn't exist, handle accordingly
|
||||||
mount_local = False
|
mount_local = False
|
||||||
@@ -265,7 +262,7 @@ def create_session(
|
|||||||
mount_local = True
|
mount_local = True
|
||||||
|
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
image_name=image_name,
|
driver_name=driver,
|
||||||
project=path_or_url,
|
project=path_or_url,
|
||||||
project_name=project,
|
project_name=project,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
@@ -285,7 +282,7 @@ def create_session(
|
|||||||
if session:
|
if session:
|
||||||
console.print("[green]Session created successfully![/green]")
|
console.print("[green]Session created successfully![/green]")
|
||||||
console.print(f"Session ID: {session.id}")
|
console.print(f"Session ID: {session.id}")
|
||||||
console.print(f"Image: {session.image}")
|
console.print(f"Driver: {session.driver}")
|
||||||
|
|
||||||
if session.ports:
|
if session.ports:
|
||||||
console.print("Ports:")
|
console.print("Ports:")
|
||||||
@@ -304,11 +301,11 @@ def create_session(
|
|||||||
if no_connect:
|
if no_connect:
|
||||||
console.print("\nConnection skipped due to --no-connect.")
|
console.print("\nConnection skipped due to --no-connect.")
|
||||||
console.print(
|
console.print(
|
||||||
f"Connect manually with:\n cubbi session connect {session.id}"
|
f"Connect manually with:\n mc session connect {session.id}"
|
||||||
)
|
)
|
||||||
elif not auto_connect:
|
elif not auto_connect:
|
||||||
console.print(
|
console.print(
|
||||||
f"\nAuto-connect disabled. Connect with:\n cubbi session connect {session.id}"
|
f"\nAuto-connect disabled. Connect with:\n mc session connect {session.id}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.print("[red]Failed to create session[/red]")
|
console.print("[red]Failed to create session[/red]")
|
||||||
@@ -319,7 +316,7 @@ def close_session(
|
|||||||
session_id: Optional[str] = typer.Argument(None, help="Session ID to close"),
|
session_id: Optional[str] = typer.Argument(None, help="Session ID to close"),
|
||||||
all_sessions: bool = typer.Option(False, "--all", help="Close all active sessions"),
|
all_sessions: bool = typer.Option(False, "--all", help="Close all active sessions"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Close a Cubbi session or all sessions"""
|
"""Close a MC session or all sessions"""
|
||||||
if all_sessions:
|
if all_sessions:
|
||||||
# Get sessions first to display them
|
# Get sessions first to display them
|
||||||
sessions = container_manager.list_sessions()
|
sessions = container_manager.list_sessions()
|
||||||
@@ -364,7 +361,7 @@ def close_session(
|
|||||||
def connect_session(
|
def connect_session(
|
||||||
session_id: str = typer.Argument(..., help="Session ID to connect to"),
|
session_id: str = typer.Argument(..., help="Session ID to connect to"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Connect to a Cubbi session"""
|
"""Connect to a MC session"""
|
||||||
console.print(f"Connecting to session {session_id}...")
|
console.print(f"Connecting to session {session_id}...")
|
||||||
success = container_manager.connect_session(session_id)
|
success = container_manager.connect_session(session_id)
|
||||||
|
|
||||||
@@ -380,7 +377,7 @@ def session_logs(
|
|||||||
False, "--init", "-i", help="Show initialization logs instead of container logs"
|
False, "--init", "-i", help="Show initialization logs instead of container logs"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stream logs from a Cubbi session"""
|
"""Stream logs from a MC session"""
|
||||||
if init:
|
if init:
|
||||||
# Show initialization logs
|
# Show initialization logs
|
||||||
if follow:
|
if follow:
|
||||||
@@ -405,13 +402,13 @@ def session_logs(
|
|||||||
console.print(logs)
|
console.print(logs)
|
||||||
|
|
||||||
|
|
||||||
@image_app.command("list")
|
@driver_app.command("list")
|
||||||
def list_images() -> None:
|
def list_drivers() -> None:
|
||||||
"""List available Cubbi images"""
|
"""List available MC drivers"""
|
||||||
images = config_manager.list_images()
|
drivers = config_manager.list_drivers()
|
||||||
|
|
||||||
if not images:
|
if not drivers:
|
||||||
console.print("No images found")
|
console.print("No drivers found")
|
||||||
return
|
return
|
||||||
|
|
||||||
table = Table(show_header=True, header_style="bold")
|
table = Table(show_header=True, header_style="bold")
|
||||||
@@ -421,92 +418,92 @@ def list_images() -> None:
|
|||||||
table.add_column("Maintainer")
|
table.add_column("Maintainer")
|
||||||
table.add_column("Image")
|
table.add_column("Image")
|
||||||
|
|
||||||
for name, image in images.items():
|
for name, driver in drivers.items():
|
||||||
table.add_row(
|
table.add_row(
|
||||||
image.name,
|
driver.name,
|
||||||
image.description,
|
driver.description,
|
||||||
image.version,
|
driver.version,
|
||||||
image.maintainer,
|
driver.maintainer,
|
||||||
image.image,
|
driver.image,
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
@image_app.command("build")
|
@driver_app.command("build")
|
||||||
def build_image(
|
def build_driver(
|
||||||
image_name: str = typer.Argument(..., help="Image name to build"),
|
driver_name: str = typer.Argument(..., help="Driver name to build"),
|
||||||
tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"),
|
tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"),
|
||||||
push: bool = typer.Option(
|
push: bool = typer.Option(
|
||||||
False, "--push", "-p", help="Push image to registry after building"
|
False, "--push", "-p", help="Push image to registry after building"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Build an image Docker image"""
|
"""Build a driver Docker image"""
|
||||||
# Get image path
|
# Get driver path
|
||||||
image_path = config_manager.get_image_path(image_name)
|
driver_path = config_manager.get_driver_path(driver_name)
|
||||||
if not image_path:
|
if not driver_path:
|
||||||
console.print(f"[red]Image '{image_name}' not found[/red]")
|
console.print(f"[red]Driver '{driver_name}' not found[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if Dockerfile exists
|
# Check if Dockerfile exists
|
||||||
dockerfile_path = image_path / "Dockerfile"
|
dockerfile_path = driver_path / "Dockerfile"
|
||||||
if not dockerfile_path.exists():
|
if not dockerfile_path.exists():
|
||||||
console.print(f"[red]Dockerfile not found in {image_path}[/red]")
|
console.print(f"[red]Dockerfile not found in {driver_path}[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Build image name
|
# Build image name
|
||||||
docker_image_name = f"monadical/cubbi-{image_name}:{tag}"
|
image_name = f"monadical/mc-{driver_name}:{tag}"
|
||||||
|
|
||||||
# Build the image
|
# Build the image
|
||||||
with console.status(f"Building image {docker_image_name}..."):
|
with console.status(f"Building image {image_name}..."):
|
||||||
result = os.system(f"cd {image_path} && docker build -t {docker_image_name} .")
|
result = os.system(f"cd {driver_path} && docker build -t {image_name} .")
|
||||||
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
console.print("[red]Failed to build image[/red]")
|
console.print("[red]Failed to build driver image[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[green]Successfully built image: {docker_image_name}[/green]")
|
console.print(f"[green]Successfully built image: {image_name}[/green]")
|
||||||
|
|
||||||
# Push if requested
|
# Push if requested
|
||||||
if push:
|
if push:
|
||||||
with console.status(f"Pushing image {docker_image_name}..."):
|
with console.status(f"Pushing image {image_name}..."):
|
||||||
result = os.system(f"docker push {docker_image_name}")
|
result = os.system(f"docker push {image_name}")
|
||||||
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
console.print("[red]Failed to push image[/red]")
|
console.print("[red]Failed to push driver image[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[green]Successfully pushed image: {docker_image_name}[/green]")
|
console.print(f"[green]Successfully pushed image: {image_name}[/green]")
|
||||||
|
|
||||||
|
|
||||||
@image_app.command("info")
|
@driver_app.command("info")
|
||||||
def image_info(
|
def driver_info(
|
||||||
image_name: str = typer.Argument(..., help="Image name to get info for"),
|
driver_name: str = typer.Argument(..., help="Driver name to get info for"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Show detailed information about an image"""
|
"""Show detailed information about a driver"""
|
||||||
image = config_manager.get_image(image_name)
|
driver = config_manager.get_driver(driver_name)
|
||||||
if not image:
|
if not driver:
|
||||||
console.print(f"[red]Image '{image_name}' not found[/red]")
|
console.print(f"[red]Driver '{driver_name}' not found[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[bold]Image: {image.name}[/bold]")
|
console.print(f"[bold]Driver: {driver.name}[/bold]")
|
||||||
console.print(f"Description: {image.description}")
|
console.print(f"Description: {driver.description}")
|
||||||
console.print(f"Version: {image.version}")
|
console.print(f"Version: {driver.version}")
|
||||||
console.print(f"Maintainer: {image.maintainer}")
|
console.print(f"Maintainer: {driver.maintainer}")
|
||||||
console.print(f"Docker Image: {image.image}")
|
console.print(f"Image: {driver.image}")
|
||||||
|
|
||||||
if image.ports:
|
if driver.ports:
|
||||||
console.print("\n[bold]Ports:[/bold]")
|
console.print("\n[bold]Ports:[/bold]")
|
||||||
for port in image.ports:
|
for port in driver.ports:
|
||||||
console.print(f" {port}")
|
console.print(f" {port}")
|
||||||
|
|
||||||
# Get image path
|
# Get driver path
|
||||||
image_path = config_manager.get_image_path(image_name)
|
driver_path = config_manager.get_driver_path(driver_name)
|
||||||
if image_path:
|
if driver_path:
|
||||||
console.print(f"\n[bold]Path:[/bold] {image_path}")
|
console.print(f"\n[bold]Path:[/bold] {driver_path}")
|
||||||
|
|
||||||
# Check for README
|
# Check for README
|
||||||
readme_path = image_path / "README.md"
|
readme_path = driver_path / "README.md"
|
||||||
if readme_path.exists():
|
if readme_path.exists():
|
||||||
console.print("\n[bold]README:[/bold]")
|
console.print("\n[bold]README:[/bold]")
|
||||||
with open(readme_path, "r") as f:
|
with open(readme_path, "r") as f:
|
||||||
@@ -1472,7 +1469,7 @@ def run_mcp_inspector(
|
|||||||
# If stop flag is set, stop all running MCP Inspectors
|
# If stop flag is set, stop all running MCP Inspectors
|
||||||
if stop:
|
if stop:
|
||||||
containers = client.containers.list(
|
containers = client.containers.list(
|
||||||
all=True, filters={"label": "cubbi.mcp.inspector=true"}
|
all=True, filters={"label": "mc.mcp.inspector=true"}
|
||||||
)
|
)
|
||||||
if not containers:
|
if not containers:
|
||||||
console.print("[yellow]No running MCP Inspector instances found[/yellow]")
|
console.print("[yellow]No running MCP Inspector instances found[/yellow]")
|
||||||
@@ -1491,7 +1488,7 @@ def run_mcp_inspector(
|
|||||||
|
|
||||||
# Check if inspector is already running
|
# Check if inspector is already running
|
||||||
all_inspectors = client.containers.list(
|
all_inspectors = client.containers.list(
|
||||||
all=True, filters={"label": "cubbi.mcp.inspector=true"}
|
all=True, filters={"label": "mc.mcp.inspector=true"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stop any existing inspectors first
|
# Stop any existing inspectors first
|
||||||
@@ -1535,7 +1532,7 @@ def run_mcp_inspector(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Container name with timestamp to avoid conflicts
|
# Container name with timestamp to avoid conflicts
|
||||||
container_name = f"cubbi_mcp_inspector_{int(time.time())}"
|
container_name = f"mc_mcp_inspector_{int(time.time())}"
|
||||||
|
|
||||||
with console.status("Starting MCP Inspector..."):
|
with console.status("Starting MCP Inspector..."):
|
||||||
# Get MCP servers from configuration
|
# Get MCP servers from configuration
|
||||||
@@ -1568,7 +1565,7 @@ def run_mcp_inspector(
|
|||||||
mcp_name = mcp.get("name")
|
mcp_name = mcp.get("name")
|
||||||
try:
|
try:
|
||||||
# Get the container name for this MCP
|
# Get the container name for this MCP
|
||||||
container_name = f"cubbi_mcp_{mcp_name}"
|
container_name = f"mc_mcp_{mcp_name}"
|
||||||
container = None
|
container = None
|
||||||
|
|
||||||
# Try to find the container
|
# Try to find the container
|
||||||
@@ -1619,7 +1616,7 @@ def run_mcp_inspector(
|
|||||||
# Make sure we have at least one network to connect to
|
# Make sure we have at least one network to connect to
|
||||||
if not mcp_networks_to_connect:
|
if not mcp_networks_to_connect:
|
||||||
# Create an MCP-specific network if none exists
|
# Create an MCP-specific network if none exists
|
||||||
network_name = "cubbi-mcp-network"
|
network_name = "mc-mcp-network"
|
||||||
console.print("No MCP networks found, creating a default one")
|
console.print("No MCP networks found, creating a default one")
|
||||||
try:
|
try:
|
||||||
networks = client.networks.list(names=[network_name])
|
networks = client.networks.list(names=[network_name])
|
||||||
@@ -1679,8 +1676,7 @@ exec npm start
|
|||||||
|
|
||||||
# Write the script to a temp file
|
# Write the script to a temp file
|
||||||
script_path = os.path.join(
|
script_path = os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
os.path.dirname(os.path.abspath(__file__)), "mc_inspector_entrypoint.sh"
|
||||||
"cubbi_inspector_entrypoint.sh",
|
|
||||||
)
|
)
|
||||||
with open(script_path, "w") as f:
|
with open(script_path, "w") as f:
|
||||||
f.write(script_content)
|
f.write(script_content)
|
||||||
@@ -1697,7 +1693,7 @@ exec npm start
|
|||||||
|
|
||||||
# Check if existing container with the same name exists, and remove it
|
# Check if existing container with the same name exists, and remove it
|
||||||
try:
|
try:
|
||||||
existing = client.containers.get("cubbi_mcp_inspector")
|
existing = client.containers.get("mc_mcp_inspector")
|
||||||
if existing.status == "running":
|
if existing.status == "running":
|
||||||
existing.stop(timeout=1)
|
existing.stop(timeout=1)
|
||||||
existing.remove(force=True)
|
existing.remove(force=True)
|
||||||
@@ -1723,7 +1719,7 @@ exec npm start
|
|||||||
for mcp in all_mcps:
|
for mcp in all_mcps:
|
||||||
if mcp.get("type") in ["docker", "proxy"]:
|
if mcp.get("type") in ["docker", "proxy"]:
|
||||||
mcp_name = mcp.get("name")
|
mcp_name = mcp.get("name")
|
||||||
container_name = f"cubbi_mcp_{mcp_name}"
|
container_name = f"mc_mcp_{mcp_name}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if this container exists
|
# Check if this container exists
|
||||||
@@ -1743,7 +1739,7 @@ exec npm start
|
|||||||
|
|
||||||
container = client.containers.run(
|
container = client.containers.run(
|
||||||
image="mcp/inspector",
|
image="mcp/inspector",
|
||||||
name="cubbi_mcp_inspector", # Use a fixed name
|
name="mc_mcp_inspector", # Use a fixed name
|
||||||
detach=True,
|
detach=True,
|
||||||
network=initial_network,
|
network=initial_network,
|
||||||
ports={
|
ports={
|
||||||
@@ -1766,8 +1762,8 @@ exec npm start
|
|||||||
},
|
},
|
||||||
entrypoint="/entrypoint.sh",
|
entrypoint="/entrypoint.sh",
|
||||||
labels={
|
labels={
|
||||||
"cubbi.mcp.inspector": "true",
|
"mc.mcp.inspector": "true",
|
||||||
"cubbi.managed": "true",
|
"mc.managed": "true",
|
||||||
},
|
},
|
||||||
network_mode=None, # Don't use network_mode as we're using network with aliases
|
network_mode=None, # Don't use network_mode as we're using network with aliases
|
||||||
networking_config=client.api.create_networking_config(network_config),
|
networking_config=client.api.create_networking_config(network_config),
|
||||||
@@ -1803,7 +1799,7 @@ exec npm start
|
|||||||
for mcp in all_mcps:
|
for mcp in all_mcps:
|
||||||
if mcp.get("type") in ["docker", "proxy"]:
|
if mcp.get("type") in ["docker", "proxy"]:
|
||||||
mcp_name = mcp.get("name")
|
mcp_name = mcp.get("name")
|
||||||
container_name = f"cubbi_mcp_{mcp_name}"
|
container_name = f"mc_mcp_{mcp_name}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if this container exists
|
# Check if this container exists
|
||||||
@@ -1875,7 +1871,7 @@ exec npm start
|
|||||||
"[yellow]Warning: No MCP servers found or started. The Inspector will run but won't have any servers to connect to.[/yellow]"
|
"[yellow]Warning: No MCP servers found or started. The Inspector will run but won't have any servers to connect to.[/yellow]"
|
||||||
)
|
)
|
||||||
console.print(
|
console.print(
|
||||||
"Start MCP servers using 'cubbi mcp start --all' and then restart the Inspector."
|
"Start MCP servers using 'mc mcp start --all' and then restart the Inspector."
|
||||||
)
|
)
|
||||||
|
|
||||||
if not detach:
|
if not detach:
|
||||||
@@ -1891,19 +1887,19 @@ exec npm start
|
|||||||
|
|
||||||
|
|
||||||
def session_create_entry_point():
|
def session_create_entry_point():
|
||||||
"""Entry point that directly invokes 'cubbi session create'.
|
"""Entry point that directly invokes 'mc session create'.
|
||||||
|
|
||||||
This provides a convenient shortcut:
|
This provides a convenient shortcut:
|
||||||
- 'cubbix' runs as if you typed 'cubbi session create'
|
- 'mcx' runs as if you typed 'mc session create'
|
||||||
- 'cubbix .' mounts the current directory
|
- 'mcx .' mounts the current directory
|
||||||
- 'cubbix /path/to/project' mounts the specified directory
|
- 'mcx /path/to/project' mounts the specified directory
|
||||||
- 'cubbix repo-url' clones the repository
|
- 'mcx repo-url' clones the repository
|
||||||
|
|
||||||
All command-line options are passed through to 'session create'.
|
All command-line options are passed through to 'session create'.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Save the program name (e.g., 'cubbix')
|
# Save the program name (e.g., 'mcx')
|
||||||
prog_name = sys.argv[0]
|
prog_name = sys.argv[0]
|
||||||
# Insert 'session' and 'create' commands before any other arguments
|
# Insert 'session' and 'create' commands before any other arguments
|
||||||
sys.argv.insert(1, "session")
|
sys.argv.insert(1, "session")
|
||||||
175
mcontainer/config.py
Normal file
175
mcontainer/config.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from .models import Config, Driver
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc"
|
||||||
|
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
||||||
|
DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers"
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
BUILTIN_DRIVERS_DIR = Path(__file__).parent / "drivers" # mcontainer/drivers
|
||||||
|
|
||||||
|
# Dynamically loaded from drivers directory at runtime
|
||||||
|
DEFAULT_DRIVERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
def __init__(self, config_path: Optional[Path] = None):
|
||||||
|
self.config_path = config_path or DEFAULT_CONFIG_FILE
|
||||||
|
self.config_dir = self.config_path.parent
|
||||||
|
self.drivers_dir = DEFAULT_DRIVERS_DIR
|
||||||
|
self.config = self._load_or_create_config()
|
||||||
|
|
||||||
|
# Always load package drivers on initialization
|
||||||
|
# These are separate from the user config
|
||||||
|
self.builtin_drivers = self._load_package_drivers()
|
||||||
|
|
||||||
|
def _load_or_create_config(self) -> Config:
|
||||||
|
"""Load existing config or create a new one with defaults"""
|
||||||
|
if self.config_path.exists():
|
||||||
|
try:
|
||||||
|
with open(self.config_path, "r") as f:
|
||||||
|
config_data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
# Create a new config from scratch, then update with data from file
|
||||||
|
config = Config(
|
||||||
|
docker=config_data.get("docker", {}),
|
||||||
|
defaults=config_data.get("defaults", {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add drivers
|
||||||
|
if "drivers" in config_data:
|
||||||
|
for driver_name, driver_data in config_data["drivers"].items():
|
||||||
|
config.drivers[driver_name] = Driver.model_validate(driver_data)
|
||||||
|
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config: {e}")
|
||||||
|
return self._create_default_config()
|
||||||
|
else:
|
||||||
|
return self._create_default_config()
|
||||||
|
|
||||||
|
def _create_default_config(self) -> Config:
|
||||||
|
"""Create a default configuration"""
|
||||||
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.drivers_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Initial config without drivers
|
||||||
|
config = Config(
|
||||||
|
docker={
|
||||||
|
"socket": "/var/run/docker.sock",
|
||||||
|
"network": "mc-network",
|
||||||
|
},
|
||||||
|
defaults={
|
||||||
|
"driver": "goose",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.save_config(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
def save_config(self, config: Optional[Config] = None) -> None:
|
||||||
|
"""Save the current config to disk"""
|
||||||
|
if config:
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Use model_dump with mode="json" for proper serialization of enums
|
||||||
|
config_dict = self.config.model_dump(mode="json")
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
with open(self.config_path, "w") as f:
|
||||||
|
yaml.dump(config_dict, f)
|
||||||
|
|
||||||
|
def get_driver(self, name: str) -> Optional[Driver]:
|
||||||
|
"""Get a driver by name, checking builtin drivers first, then user-configured ones"""
|
||||||
|
# Check builtin drivers first (package drivers take precedence)
|
||||||
|
if name in self.builtin_drivers:
|
||||||
|
return self.builtin_drivers[name]
|
||||||
|
# If not found, check user-configured drivers
|
||||||
|
return self.config.drivers.get(name)
|
||||||
|
|
||||||
|
def list_drivers(self) -> Dict[str, Driver]:
|
||||||
|
"""List all available drivers (both builtin and user-configured)"""
|
||||||
|
# Start with user config drivers
|
||||||
|
all_drivers = dict(self.config.drivers)
|
||||||
|
|
||||||
|
# Add builtin drivers, overriding any user drivers with the same name
|
||||||
|
# This ensures that package-provided drivers always take precedence
|
||||||
|
all_drivers.update(self.builtin_drivers)
|
||||||
|
|
||||||
|
return all_drivers
|
||||||
|
|
||||||
|
# Session management has been moved to SessionManager in session.py
|
||||||
|
|
||||||
|
def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]:
|
||||||
|
"""Load a driver configuration from a directory"""
|
||||||
|
# Try with mc-driver.yaml first (new format), then mai-driver.yaml (legacy)
|
||||||
|
yaml_path = driver_dir / "mc-driver.yaml"
|
||||||
|
if not yaml_path.exists():
|
||||||
|
yaml_path = driver_dir / "mai-driver.yaml" # Backward compatibility
|
||||||
|
if not yaml_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(yaml_path, "r") as f:
|
||||||
|
driver_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Extract required fields
|
||||||
|
if not all(
|
||||||
|
k in driver_data
|
||||||
|
for k in ["name", "description", "version", "maintainer"]
|
||||||
|
):
|
||||||
|
print(f"Driver config {yaml_path} missing required fields")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use Driver.model_validate to handle all fields from YAML
|
||||||
|
# This will map all fields according to the Driver model structure
|
||||||
|
try:
|
||||||
|
# Ensure image field is set if not in YAML
|
||||||
|
if "image" not in driver_data:
|
||||||
|
driver_data["image"] = f"monadical/mc-{driver_data['name']}:latest"
|
||||||
|
|
||||||
|
driver = Driver.model_validate(driver_data)
|
||||||
|
return driver
|
||||||
|
except Exception as validation_error:
|
||||||
|
print(
|
||||||
|
f"Error validating driver data from {yaml_path}: {validation_error}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading driver from {yaml_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_package_drivers(self) -> Dict[str, Driver]:
|
||||||
|
"""Load all package drivers from the mcontainer/drivers directory"""
|
||||||
|
drivers = {}
|
||||||
|
|
||||||
|
if not BUILTIN_DRIVERS_DIR.exists():
|
||||||
|
return drivers
|
||||||
|
|
||||||
|
# Search for mc-driver.yaml files in each subdirectory
|
||||||
|
for driver_dir in BUILTIN_DRIVERS_DIR.iterdir():
|
||||||
|
if driver_dir.is_dir():
|
||||||
|
driver = self.load_driver_from_dir(driver_dir)
|
||||||
|
if driver:
|
||||||
|
drivers[driver.name] = driver
|
||||||
|
|
||||||
|
return drivers
|
||||||
|
|
||||||
|
def get_driver_path(self, driver_name: str) -> Optional[Path]:
|
||||||
|
"""Get the directory path for a driver"""
|
||||||
|
# Check package drivers first (these are the bundled ones)
|
||||||
|
package_path = BUILTIN_DRIVERS_DIR / driver_name
|
||||||
|
if package_path.exists() and package_path.is_dir():
|
||||||
|
return package_path
|
||||||
|
|
||||||
|
# Then check user drivers
|
||||||
|
user_path = self.drivers_dir / driver_name
|
||||||
|
if user_path.exists() and user_path.is_dir():
|
||||||
|
return user_path
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import concurrent.futures
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
import hashlib
|
||||||
|
import pathlib
|
||||||
|
import concurrent.futures
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
from docker.errors import DockerException, ImageNotFound
|
from docker.errors import DockerException, ImageNotFound
|
||||||
|
|
||||||
from .config import ConfigManager
|
|
||||||
from .mcp import MCPManager
|
|
||||||
from .models import Session, SessionStatus
|
from .models import Session, SessionStatus
|
||||||
|
from .config import ConfigManager
|
||||||
from .session import SessionManager
|
from .session import SessionManager
|
||||||
|
from .mcp import MCPManager
|
||||||
from .user_config import UserConfigManager
|
from .user_config import UserConfigManager
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -42,8 +41,8 @@ class ContainerManager:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def _ensure_network(self) -> None:
|
def _ensure_network(self) -> None:
|
||||||
"""Ensure the Cubbi network exists"""
|
"""Ensure the MC network exists"""
|
||||||
network_name = self.config_manager.config.docker.get("network", "cubbi-network")
|
network_name = self.config_manager.config.docker.get("network", "mc-network")
|
||||||
networks = self.client.networks.list(names=[network_name])
|
networks = self.client.networks.list(names=[network_name])
|
||||||
if not networks:
|
if not networks:
|
||||||
self.client.networks.create(network_name, driver="bridge")
|
self.client.networks.create(network_name, driver="bridge")
|
||||||
@@ -64,8 +63,8 @@ class ContainerManager:
|
|||||||
Returns:
|
Returns:
|
||||||
Path to the project configuration directory, or None if no project_name is provided
|
Path to the project configuration directory, or None if no project_name is provided
|
||||||
"""
|
"""
|
||||||
# Get home directory for the Cubbi config
|
# Get home directory for the MC config
|
||||||
cubbi_home = pathlib.Path.home() / ".cubbi"
|
mc_home = pathlib.Path.home() / ".mc"
|
||||||
|
|
||||||
# Only use project_name if explicitly provided
|
# Only use project_name if explicitly provided
|
||||||
if project_name:
|
if project_name:
|
||||||
@@ -73,7 +72,7 @@ class ContainerManager:
|
|||||||
project_hash = hashlib.md5(project_name.encode()).hexdigest()
|
project_hash = hashlib.md5(project_name.encode()).hexdigest()
|
||||||
|
|
||||||
# Create the project config directory path
|
# Create the project config directory path
|
||||||
config_path = cubbi_home / "projects" / project_hash / "config"
|
config_path = mc_home / "projects" / project_hash / "config"
|
||||||
|
|
||||||
# Create the directory if it doesn't exist
|
# Create the directory if it doesn't exist
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -82,22 +81,22 @@ class ContainerManager:
|
|||||||
return config_path
|
return config_path
|
||||||
else:
|
else:
|
||||||
# If no project_name is provided, don't create any config directory
|
# If no project_name is provided, don't create any config directory
|
||||||
# This ensures we don't mount the /cubbi-config volume for project-less sessions
|
# This ensures we don't mount the /mc-config volume for project-less sessions
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def list_sessions(self) -> List[Session]:
|
def list_sessions(self) -> List[Session]:
|
||||||
"""List all active Cubbi sessions"""
|
"""List all active MC sessions"""
|
||||||
sessions = []
|
sessions = []
|
||||||
try:
|
try:
|
||||||
containers = self.client.containers.list(
|
containers = self.client.containers.list(
|
||||||
all=True, filters={"label": "cubbi.session"}
|
all=True, filters={"label": "mc.session"}
|
||||||
)
|
)
|
||||||
|
|
||||||
for container in containers:
|
for container in containers:
|
||||||
container_id = container.id
|
container_id = container.id
|
||||||
labels = container.labels
|
labels = container.labels
|
||||||
|
|
||||||
session_id = labels.get("cubbi.session.id")
|
session_id = labels.get("mc.session.id")
|
||||||
if not session_id:
|
if not session_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -109,8 +108,8 @@ class ContainerManager:
|
|||||||
|
|
||||||
session = Session(
|
session = Session(
|
||||||
id=session_id,
|
id=session_id,
|
||||||
name=labels.get("cubbi.session.name", f"cubbi-{session_id}"),
|
name=labels.get("mc.session.name", f"mc-{session_id}"),
|
||||||
image=labels.get("cubbi.image", "unknown"),
|
driver=labels.get("mc.driver", "unknown"),
|
||||||
status=status,
|
status=status,
|
||||||
container_id=container_id,
|
container_id=container_id,
|
||||||
)
|
)
|
||||||
@@ -137,7 +136,7 @@ class ContainerManager:
|
|||||||
|
|
||||||
def create_session(
|
def create_session(
|
||||||
self,
|
self,
|
||||||
image_name: str,
|
driver_name: str,
|
||||||
project: Optional[str] = None,
|
project: Optional[str] = None,
|
||||||
project_name: Optional[str] = None,
|
project_name: Optional[str] = None,
|
||||||
environment: Optional[Dict[str, str]] = None,
|
environment: Optional[Dict[str, str]] = None,
|
||||||
@@ -153,10 +152,10 @@ class ContainerManager:
|
|||||||
provider: Optional[str] = None,
|
provider: Optional[str] = None,
|
||||||
ssh: bool = False,
|
ssh: bool = False,
|
||||||
) -> Optional[Session]:
|
) -> Optional[Session]:
|
||||||
"""Create a new Cubbi session
|
"""Create a new MC session
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_name: The name of the image to use
|
driver_name: The name of the driver to use
|
||||||
project: Optional project repository URL or local directory path
|
project: Optional project repository URL or local directory path
|
||||||
project_name: Optional explicit project name for configuration persistence
|
project_name: Optional explicit project name for configuration persistence
|
||||||
environment: Optional environment variables
|
environment: Optional environment variables
|
||||||
@@ -171,16 +170,16 @@ class ContainerManager:
|
|||||||
ssh: Whether to start the SSH server in the container (default: False)
|
ssh: Whether to start the SSH server in the container (default: False)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate image exists
|
# Validate driver exists
|
||||||
image = self.config_manager.get_image(image_name)
|
driver = self.config_manager.get_driver(driver_name)
|
||||||
if not image:
|
if not driver:
|
||||||
print(f"Image '{image_name}' not found")
|
print(f"Driver '{driver_name}' not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Generate session ID and name
|
# Generate session ID and name
|
||||||
session_id = self._generate_session_id()
|
session_id = self._generate_session_id()
|
||||||
if not session_name:
|
if not session_name:
|
||||||
session_name = f"cubbi-{session_id}"
|
session_name = f"mc-{session_id}"
|
||||||
|
|
||||||
# Ensure network exists
|
# Ensure network exists
|
||||||
self._ensure_network()
|
self._ensure_network()
|
||||||
@@ -188,12 +187,12 @@ class ContainerManager:
|
|||||||
# Prepare environment variables
|
# Prepare environment variables
|
||||||
env_vars = environment or {}
|
env_vars = environment or {}
|
||||||
|
|
||||||
# Add CUBBI_USER_ID and CUBBI_GROUP_ID for entrypoint script
|
# Add MC_USER_ID and MC_GROUP_ID for entrypoint script
|
||||||
env_vars["CUBBI_USER_ID"] = str(uid) if uid is not None else "1000"
|
env_vars["MC_USER_ID"] = str(uid) if uid is not None else "1000"
|
||||||
env_vars["CUBBI_GROUP_ID"] = str(gid) if gid is not None else "1000"
|
env_vars["MC_GROUP_ID"] = str(gid) if gid is not None else "1000"
|
||||||
|
|
||||||
# Set SSH environment variable
|
# Set SSH environment variable
|
||||||
env_vars["CUBBI_SSH_ENABLED"] = "true" if ssh else "false"
|
env_vars["MC_SSH_ENABLED"] = "true" if ssh else "false"
|
||||||
|
|
||||||
# Pass API keys from host environment to container for local development
|
# Pass API keys from host environment to container for local development
|
||||||
api_keys = [
|
api_keys = [
|
||||||
@@ -211,10 +210,10 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Pull image if needed
|
# Pull image if needed
|
||||||
try:
|
try:
|
||||||
self.client.images.get(image.image)
|
self.client.images.get(driver.image)
|
||||||
except ImageNotFound:
|
except ImageNotFound:
|
||||||
print(f"Pulling image {image.image}...")
|
print(f"Pulling image {driver.image}...")
|
||||||
self.client.images.pull(image.image)
|
self.client.images.pull(driver.image)
|
||||||
|
|
||||||
# Set up volume mounts
|
# Set up volume mounts
|
||||||
session_volumes = {}
|
session_volumes = {}
|
||||||
@@ -240,7 +239,7 @@ class ContainerManager:
|
|||||||
# Clear project for container environment since we're mounting
|
# Clear project for container environment since we're mounting
|
||||||
project = None
|
project = None
|
||||||
elif is_git_repo:
|
elif is_git_repo:
|
||||||
env_vars["CUBBI_PROJECT_URL"] = project
|
env_vars["MC_PROJECT_URL"] = project
|
||||||
print(
|
print(
|
||||||
f"Git repository URL provided - container will clone {project} into /app during initialization"
|
f"Git repository URL provided - container will clone {project} into /app during initialization"
|
||||||
)
|
)
|
||||||
@@ -269,22 +268,22 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Mount the project configuration directory
|
# Mount the project configuration directory
|
||||||
session_volumes[str(project_config_path)] = {
|
session_volumes[str(project_config_path)] = {
|
||||||
"bind": "/cubbi-config",
|
"bind": "/mc-config",
|
||||||
"mode": "rw",
|
"mode": "rw",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add environment variables for config path
|
# Add environment variables for config path
|
||||||
env_vars["CUBBI_CONFIG_DIR"] = "/cubbi-config"
|
env_vars["MC_CONFIG_DIR"] = "/mc-config"
|
||||||
env_vars["CUBBI_IMAGE_CONFIG_DIR"] = f"/cubbi-config/{image_name}"
|
env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}"
|
||||||
|
|
||||||
# Create image-specific config directories and set up direct volume mounts
|
# Create driver-specific config directories and set up direct volume mounts
|
||||||
if image.persistent_configs:
|
if driver.persistent_configs:
|
||||||
persistent_links_data = [] # To store "source:target" pairs for symlinks
|
persistent_links_data = [] # To store "source:target" pairs for symlinks
|
||||||
print("Setting up persistent configuration directories:")
|
print("Setting up persistent configuration directories:")
|
||||||
for config in image.persistent_configs:
|
for config in driver.persistent_configs:
|
||||||
# Get target directory path on host
|
# Get target directory path on host
|
||||||
target_dir = project_config_path / config.target.removeprefix(
|
target_dir = project_config_path / config.target.removeprefix(
|
||||||
"/cubbi-config/"
|
"/mc-config/"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create directory if it's a directory type config
|
# Create directory if it's a directory type config
|
||||||
@@ -299,7 +298,7 @@ class ContainerManager:
|
|||||||
# File will be created by the container if needed
|
# File will be created by the container if needed
|
||||||
|
|
||||||
# Store the source and target paths for the init script
|
# Store the source and target paths for the init script
|
||||||
# Note: config.target is the path *within* /cubbi-config
|
# Note: config.target is the path *within* /mc-config
|
||||||
persistent_links_data.append(f"{config.source}:{config.target}")
|
persistent_links_data.append(f"{config.source}:{config.target}")
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -308,20 +307,20 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Set up persistent links
|
# Set up persistent links
|
||||||
if persistent_links_data:
|
if persistent_links_data:
|
||||||
env_vars["CUBBI_PERSISTENT_LINKS"] = ";".join(
|
env_vars["MC_PERSISTENT_LINKS"] = ";".join(
|
||||||
persistent_links_data
|
persistent_links_data
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f"Setting CUBBI_PERSISTENT_LINKS={env_vars['CUBBI_PERSISTENT_LINKS']}"
|
f"Setting MC_PERSISTENT_LINKS={env_vars['MC_PERSISTENT_LINKS']}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"No project_name provided - skipping configuration directory setup."
|
"No project_name provided - skipping configuration directory setup."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Default Cubbi network
|
# Default MC network
|
||||||
default_network = self.config_manager.config.docker.get(
|
default_network = self.config_manager.config.docker.get(
|
||||||
"network", "cubbi-network"
|
"network", "mc-network"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get network list
|
# Get network list
|
||||||
@@ -440,9 +439,9 @@ class ContainerManager:
|
|||||||
env_vars["MCP_NAMES"] = json.dumps(mcp_names)
|
env_vars["MCP_NAMES"] = json.dumps(mcp_names)
|
||||||
|
|
||||||
# Add user-specified networks
|
# Add user-specified networks
|
||||||
# Default Cubbi network
|
# Default MC network
|
||||||
default_network = self.config_manager.config.docker.get(
|
default_network = self.config_manager.config.docker.get(
|
||||||
"network", "cubbi-network"
|
"network", "mc-network"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get network list, ensuring default is first and no duplicates
|
# Get network list, ensuring default is first and no duplicates
|
||||||
@@ -468,12 +467,12 @@ class ContainerManager:
|
|||||||
target_shell = "/bin/bash"
|
target_shell = "/bin/bash"
|
||||||
|
|
||||||
if run_command:
|
if run_command:
|
||||||
# Set environment variable for cubbi-init.sh to pick up
|
# Set environment variable for mc-init.sh to pick up
|
||||||
env_vars["CUBBI_RUN_COMMAND"] = run_command
|
env_vars["MC_RUN_COMMAND"] = run_command
|
||||||
# Set the container's command to be the final shell
|
# Set the container's command to be the final shell
|
||||||
container_command = [target_shell]
|
container_command = [target_shell]
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Setting CUBBI_RUN_COMMAND and targeting shell {target_shell}"
|
f"Setting MC_RUN_COMMAND and targeting shell {target_shell}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Use default behavior (often defined by image's ENTRYPOINT/CMD)
|
# Use default behavior (often defined by image's ENTRYPOINT/CMD)
|
||||||
@@ -486,16 +485,16 @@ class ContainerManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Set default model/provider from user config if not explicitly provided
|
# Set default model/provider from user config if not explicitly provided
|
||||||
env_vars["CUBBI_MODEL"] = model or self.user_config_manager.get(
|
env_vars["MC_MODEL"] = model or self.user_config_manager.get(
|
||||||
"defaults.model", ""
|
"defaults.model", ""
|
||||||
)
|
)
|
||||||
env_vars["CUBBI_PROVIDER"] = provider or self.user_config_manager.get(
|
env_vars["MC_PROVIDER"] = provider or self.user_config_manager.get(
|
||||||
"defaults.provider", ""
|
"defaults.provider", ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create container
|
# Create container
|
||||||
container = self.client.containers.create(
|
container = self.client.containers.create(
|
||||||
image=image.image,
|
image=driver.image,
|
||||||
name=session_name,
|
name=session_name,
|
||||||
hostname=session_name,
|
hostname=session_name,
|
||||||
detach=True,
|
detach=True,
|
||||||
@@ -504,18 +503,18 @@ class ContainerManager:
|
|||||||
environment=env_vars,
|
environment=env_vars,
|
||||||
volumes=session_volumes,
|
volumes=session_volumes,
|
||||||
labels={
|
labels={
|
||||||
"cubbi.session": "true",
|
"mc.session": "true",
|
||||||
"cubbi.session.id": session_id,
|
"mc.session.id": session_id,
|
||||||
"cubbi.session.name": session_name,
|
"mc.session.name": session_name,
|
||||||
"cubbi.image": image_name,
|
"mc.driver": driver_name,
|
||||||
"cubbi.project": project or "",
|
"mc.project": project or "",
|
||||||
"cubbi.project_name": project_name or "",
|
"mc.project_name": project_name or "",
|
||||||
"cubbi.mcps": ",".join(mcp_names) if mcp_names else "",
|
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
|
||||||
},
|
},
|
||||||
network=network_list[0], # Connect to the first network initially
|
network=network_list[0], # Connect to the first network initially
|
||||||
command=container_command, # Set the command
|
command=container_command, # Set the command
|
||||||
entrypoint=entrypoint, # Set the entrypoint (might be None)
|
entrypoint=entrypoint, # Set the entrypoint (might be None)
|
||||||
ports={f"{port}/tcp": None for port in image.ports},
|
ports={f"{port}/tcp": None for port in driver.ports},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start container
|
# Start container
|
||||||
@@ -549,7 +548,7 @@ class ContainerManager:
|
|||||||
for mcp_name in mcp_names:
|
for mcp_name in mcp_names:
|
||||||
try:
|
try:
|
||||||
# Get the dedicated network for this MCP
|
# Get the dedicated network for this MCP
|
||||||
dedicated_network_name = f"cubbi-mcp-{mcp_name}-network"
|
dedicated_network_name = f"mc-mcp-{mcp_name}-network"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
network = self.client.networks.get(dedicated_network_name)
|
network = self.client.networks.get(dedicated_network_name)
|
||||||
@@ -614,7 +613,7 @@ class ContainerManager:
|
|||||||
session = Session(
|
session = Session(
|
||||||
id=session_id,
|
id=session_id,
|
||||||
name=session_name,
|
name=session_name,
|
||||||
image=image_name,
|
driver=driver_name,
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
container_id=container.id,
|
container_id=container.id,
|
||||||
ports=ports,
|
ports=ports,
|
||||||
@@ -633,7 +632,7 @@ class ContainerManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def close_session(self, session_id: str) -> bool:
|
def close_session(self, session_id: str) -> bool:
|
||||||
"""Close a Cubbi session"""
|
"""Close a MC session"""
|
||||||
try:
|
try:
|
||||||
sessions = self.list_sessions()
|
sessions = self.list_sessions()
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
@@ -648,7 +647,7 @@ class ContainerManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def connect_session(self, session_id: str) -> bool:
|
def connect_session(self, session_id: str) -> bool:
|
||||||
"""Connect to a running Cubbi session"""
|
"""Connect to a running MC session"""
|
||||||
# Retrieve full session data which should include uid/gid
|
# Retrieve full session data which should include uid/gid
|
||||||
session_data = self.session_manager.get_session(session_id)
|
session_data = self.session_manager.get_session(session_id)
|
||||||
|
|
||||||
@@ -738,7 +737,7 @@ class ContainerManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def close_all_sessions(self, progress_callback=None) -> Tuple[int, bool]:
|
def close_all_sessions(self, progress_callback=None) -> Tuple[int, bool]:
|
||||||
"""Close all Cubbi sessions with parallel processing and progress reporting
|
"""Close all MC sessions with parallel processing and progress reporting
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
progress_callback: Optional callback function to report progress
|
progress_callback: Optional callback function to report progress
|
||||||
@@ -811,7 +810,7 @@ class ContainerManager:
|
|||||||
return 0, False
|
return 0, False
|
||||||
|
|
||||||
def get_session_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
|
def get_session_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
|
||||||
"""Get logs from a Cubbi session"""
|
"""Get logs from a MC session"""
|
||||||
try:
|
try:
|
||||||
sessions = self.list_sessions()
|
sessions = self.list_sessions()
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
@@ -832,7 +831,7 @@ class ContainerManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_init_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
|
def get_init_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
|
||||||
"""Get initialization logs from a Cubbi session
|
"""Get initialization logs from a MC session
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: The session ID
|
session_id: The session ID
|
||||||
28
mcontainer/drivers/base.py
Normal file
28
mcontainer/drivers/base.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Base driver implementation for MAI
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from ..models import Driver
|
||||||
|
|
||||||
|
|
||||||
|
class DriverManager:
|
||||||
|
"""Manager for MAI drivers"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_drivers() -> Dict[str, Driver]:
|
||||||
|
"""Get the default built-in drivers"""
|
||||||
|
from ..config import DEFAULT_DRIVERS
|
||||||
|
|
||||||
|
return DEFAULT_DRIVERS
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_driver_metadata(driver_name: str) -> Optional[Dict]:
|
||||||
|
"""Get metadata for a specific driver"""
|
||||||
|
from ..config import DEFAULT_DRIVERS
|
||||||
|
|
||||||
|
if driver_name in DEFAULT_DRIVERS:
|
||||||
|
return DEFAULT_DRIVERS[driver_name].model_dump()
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
LABEL maintainer="team@monadical.com"
|
LABEL maintainer="team@monadical.com"
|
||||||
LABEL description="Goose with MCP servers for Cubbi"
|
LABEL description="Goose with MCP servers"
|
||||||
|
|
||||||
# Install system dependencies including gosu for user switching and shadow for useradd/groupadd
|
# Install system dependencies including gosu for user switching and shadow for useradd/groupadd
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
@@ -41,16 +41,16 @@ RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy initialization scripts
|
# Copy initialization scripts
|
||||||
COPY cubbi-init.sh /cubbi-init.sh
|
COPY mc-init.sh /mc-init.sh
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
COPY cubbi-image.yaml /cubbi-image.yaml
|
COPY mc-driver.yaml /mc-driver.yaml
|
||||||
COPY init-status.sh /init-status.sh
|
COPY init-status.sh /init-status.sh
|
||||||
COPY update-goose-config.py /usr/local/bin/update-goose-config.py
|
COPY update-goose-config.py /usr/local/bin/update-goose-config.py
|
||||||
|
|
||||||
# Extend env via bashrc
|
# Extend env via bashrc
|
||||||
|
|
||||||
# Make scripts executable
|
# Make scripts executable
|
||||||
RUN chmod +x /cubbi-init.sh /entrypoint.sh /init-status.sh \
|
RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \
|
||||||
/usr/local/bin/update-goose-config.py
|
/usr/local/bin/update-goose-config.py
|
||||||
|
|
||||||
# Set up initialization status check on login
|
# Set up initialization status check on login
|
||||||
@@ -59,7 +59,7 @@ RUN echo '[ -x /init-status.sh ] && /init-status.sh' >> /etc/bash.bashrc
|
|||||||
# Set up environment
|
# Set up environment
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
# Set WORKDIR to /app, common practice and expected by cubbi-init.sh
|
# Set WORKDIR to /app, common practice and expected by mc-init.sh
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Expose ports
|
# Expose ports
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Goose Image for MC
|
# Goose Driver for MC
|
||||||
|
|
||||||
This image provides a containerized environment for running [Goose](https://goose.ai).
|
This driver provides a containerized environment for running [Goose](https://goose.ai).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -17,25 +17,25 @@ This image provides a containerized environment for running [Goose](https://goos
|
|||||||
| `LANGFUSE_INIT_PROJECT_PUBLIC_KEY` | Langfuse public key | No |
|
| `LANGFUSE_INIT_PROJECT_PUBLIC_KEY` | Langfuse public key | No |
|
||||||
| `LANGFUSE_INIT_PROJECT_SECRET_KEY` | Langfuse secret key | No |
|
| `LANGFUSE_INIT_PROJECT_SECRET_KEY` | Langfuse secret key | No |
|
||||||
| `LANGFUSE_URL` | Langfuse API URL | No |
|
| `LANGFUSE_URL` | Langfuse API URL | No |
|
||||||
| `CUBBI_PROJECT_URL` | Project repository URL | No |
|
| `MC_PROJECT_URL` | Project repository URL | No |
|
||||||
| `CUBBI_GIT_SSH_KEY` | SSH key for Git authentication | No |
|
| `MC_GIT_SSH_KEY` | SSH key for Git authentication | No |
|
||||||
| `CUBBI_GIT_TOKEN` | Token for Git authentication | No |
|
| `MC_GIT_TOKEN` | Token for Git authentication | No |
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
To build this image:
|
To build this driver:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd drivers/goose
|
cd drivers/goose
|
||||||
docker build -t monadical/cubbi-goose:latest .
|
docker build -t monadical/mc-goose:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new session with this image
|
# Create a new session with this driver
|
||||||
cubbi session create --driver goose
|
mc session create --driver goose
|
||||||
|
|
||||||
# Create with project repository
|
# Create with project repository
|
||||||
cubbi session create --driver goose --project github.com/username/repo
|
mc session create --driver goose --project github.com/username/repo
|
||||||
```
|
```
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Entrypoint script for Goose image
|
# Entrypoint script for Goose driver
|
||||||
# Executes the standard initialization script, which handles user setup,
|
# Executes the standard initialization script, which handles user setup,
|
||||||
# service startup (like sshd), and switching to the non-root user
|
# service startup (like sshd), and switching to the non-root user
|
||||||
# before running the container's command (CMD).
|
# before running the container's command (CMD).
|
||||||
|
|
||||||
exec /cubbi-init.sh "$@"
|
exec /mc-init.sh "$@"
|
||||||
@@ -28,4 +28,4 @@ if ! grep -q "INIT_COMPLETE=true" "/init.status" 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec gosu cubbi /bin/bash -il
|
exec gosu mcuser /bin/bash -il
|
||||||
@@ -2,10 +2,10 @@ name: goose
|
|||||||
description: Goose AI environment
|
description: Goose AI environment
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
maintainer: team@monadical.com
|
maintainer: team@monadical.com
|
||||||
image: monadical/cubbi-goose:latest
|
image: monadical/mc-goose:latest
|
||||||
|
|
||||||
init:
|
init:
|
||||||
pre_command: /cubbi-init.sh
|
pre_command: /mc-init.sh
|
||||||
command: /entrypoint.sh
|
command: /entrypoint.sh
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -25,21 +25,21 @@ environment:
|
|||||||
default: https://cloud.langfuse.com
|
default: https://cloud.langfuse.com
|
||||||
|
|
||||||
# Project environment variables
|
# Project environment variables
|
||||||
- name: CUBBI_PROJECT_URL
|
- name: MC_PROJECT_URL
|
||||||
description: Project repository URL
|
description: Project repository URL
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- name: CUBBI_PROJECT_TYPE
|
- name: MC_PROJECT_TYPE
|
||||||
description: Project repository type (git, svn, etc.)
|
description: Project repository type (git, svn, etc.)
|
||||||
required: false
|
required: false
|
||||||
default: git
|
default: git
|
||||||
|
|
||||||
- name: CUBBI_GIT_SSH_KEY
|
- name: MC_GIT_SSH_KEY
|
||||||
description: SSH key for Git authentication
|
description: SSH key for Git authentication
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
|
|
||||||
- name: CUBBI_GIT_TOKEN
|
- name: MC_GIT_TOKEN
|
||||||
description: Token for Git authentication
|
description: Token for Git authentication
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
@@ -54,10 +54,10 @@ volumes:
|
|||||||
|
|
||||||
persistent_configs:
|
persistent_configs:
|
||||||
- source: "/app/.goose"
|
- source: "/app/.goose"
|
||||||
target: "/cubbi-config/goose-app"
|
target: "/mc-config/goose-app"
|
||||||
type: "directory"
|
type: "directory"
|
||||||
description: "Goose memory"
|
description: "Goose memory"
|
||||||
- source: "/home/cubbi/.config/goose"
|
- source: "/home/mcuser/.config/goose"
|
||||||
target: "/cubbi-config/goose-config"
|
target: "/mc-config/goose-config"
|
||||||
type: "directory"
|
type: "directory"
|
||||||
description: "Goose configuration"
|
description: "Goose configuration"
|
||||||
@@ -1,59 +1,59 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Standardized initialization script for Cubbi images
|
# Standardized initialization script for MC drivers
|
||||||
|
|
||||||
# Redirect all output to both stdout and the log file
|
# Redirect all output to both stdout and the log file
|
||||||
exec > >(tee -a /init.log) 2>&1
|
exec > >(tee -a /init.log) 2>&1
|
||||||
|
|
||||||
# Mark initialization as started
|
# Mark initialization as started
|
||||||
echo "=== Cubbi Initialization started at $(date) ==="
|
echo "=== MC Initialization started at $(date) ==="
|
||||||
|
|
||||||
# --- START INSERTED BLOCK ---
|
# --- START INSERTED BLOCK ---
|
||||||
|
|
||||||
# Default UID/GID if not provided (should be passed by cubbi tool)
|
# Default UID/GID if not provided (should be passed by mc tool)
|
||||||
CUBBI_USER_ID=${CUBBI_USER_ID:-1000}
|
MC_USER_ID=${MC_USER_ID:-1000}
|
||||||
CUBBI_GROUP_ID=${CUBBI_GROUP_ID:-1000}
|
MC_GROUP_ID=${MC_GROUP_ID:-1000}
|
||||||
|
|
||||||
echo "Using UID: $CUBBI_USER_ID, GID: $CUBBI_GROUP_ID"
|
echo "Using UID: $MC_USER_ID, GID: $MC_GROUP_ID"
|
||||||
|
|
||||||
# Create group if it doesn't exist
|
# Create group if it doesn't exist
|
||||||
if ! getent group cubbi > /dev/null; then
|
if ! getent group mcuser > /dev/null; then
|
||||||
groupadd -g $CUBBI_GROUP_ID cubbi
|
groupadd -g $MC_GROUP_ID mcuser
|
||||||
else
|
else
|
||||||
# If group exists but has different GID, modify it
|
# If group exists but has different GID, modify it
|
||||||
EXISTING_GID=$(getent group cubbi | cut -d: -f3)
|
EXISTING_GID=$(getent group mcuser | cut -d: -f3)
|
||||||
if [ "$EXISTING_GID" != "$CUBBI_GROUP_ID" ]; then
|
if [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
|
||||||
groupmod -g $CUBBI_GROUP_ID cubbi
|
groupmod -g $MC_GROUP_ID mcuser
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create user if it doesn't exist
|
# Create user if it doesn't exist
|
||||||
if ! getent passwd cubbi > /dev/null; then
|
if ! getent passwd mcuser > /dev/null; then
|
||||||
useradd --shell /bin/bash --uid $CUBBI_USER_ID --gid $CUBBI_GROUP_ID --no-create-home cubbi
|
useradd --shell /bin/bash --uid $MC_USER_ID --gid $MC_GROUP_ID --no-create-home mcuser
|
||||||
else
|
else
|
||||||
# If user exists but has different UID/GID, modify it
|
# If user exists but has different UID/GID, modify it
|
||||||
EXISTING_UID=$(getent passwd cubbi | cut -d: -f3)
|
EXISTING_UID=$(getent passwd mcuser | cut -d: -f3)
|
||||||
EXISTING_GID=$(getent passwd cubbi | cut -d: -f4)
|
EXISTING_GID=$(getent passwd mcuser | cut -d: -f4)
|
||||||
if [ "$EXISTING_UID" != "$CUBBI_USER_ID" ] || [ "$EXISTING_GID" != "$CUBBI_GROUP_ID" ]; then
|
if [ "$EXISTING_UID" != "$MC_USER_ID" ] || [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
|
||||||
usermod --uid $CUBBI_USER_ID --gid $CUBBI_GROUP_ID cubbi
|
usermod --uid $MC_USER_ID --gid $MC_GROUP_ID mcuser
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create home directory and set permissions
|
# Create home directory and set permissions
|
||||||
mkdir -p /home/cubbi
|
mkdir -p /home/mcuser
|
||||||
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /home/cubbi
|
chown $MC_USER_ID:$MC_GROUP_ID /home/mcuser
|
||||||
mkdir -p /app
|
mkdir -p /app
|
||||||
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /app
|
chown $MC_USER_ID:$MC_GROUP_ID /app
|
||||||
|
|
||||||
# Copy /root/.local/bin to the user's home directory
|
# Copy /root/.local/bin to the user's home directory
|
||||||
if [ -d /root/.local/bin ]; then
|
if [ -d /root/.local/bin ]; then
|
||||||
echo "Copying /root/.local/bin to /home/cubbi/.local/bin..."
|
echo "Copying /root/.local/bin to /home/mcuser/.local/bin..."
|
||||||
mkdir -p /home/cubbi/.local/bin
|
mkdir -p /home/mcuser/.local/bin
|
||||||
cp -r /root/.local/bin/* /home/cubbi/.local/bin/
|
cp -r /root/.local/bin/* /home/mcuser/.local/bin/
|
||||||
chown -R $CUBBI_USER_ID:$CUBBI_GROUP_ID /home/cubbi/.local
|
chown -R $MC_USER_ID:$MC_GROUP_ID /home/mcuser/.local
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start SSH server only if explicitly enabled
|
# Start SSH server only if explicitly enabled
|
||||||
if [ "$CUBBI_SSH_ENABLED" = "true" ]; then
|
if [ "$MC_SSH_ENABLED" = "true" ]; then
|
||||||
echo "Starting SSH server..."
|
echo "Starting SSH server..."
|
||||||
/usr/sbin/sshd
|
/usr/sbin/sshd
|
||||||
else
|
else
|
||||||
@@ -65,13 +65,13 @@ fi
|
|||||||
echo "INIT_COMPLETE=false" > /init.status
|
echo "INIT_COMPLETE=false" > /init.status
|
||||||
|
|
||||||
# Project initialization
|
# Project initialization
|
||||||
if [ -n "$CUBBI_PROJECT_URL" ]; then
|
if [ -n "$MC_PROJECT_URL" ]; then
|
||||||
echo "Initializing project: $CUBBI_PROJECT_URL"
|
echo "Initializing project: $MC_PROJECT_URL"
|
||||||
|
|
||||||
# Set up SSH key if provided
|
# Set up SSH key if provided
|
||||||
if [ -n "$CUBBI_GIT_SSH_KEY" ]; then
|
if [ -n "$MC_GIT_SSH_KEY" ]; then
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "$CUBBI_GIT_SSH_KEY" > ~/.ssh/id_ed25519
|
echo "$MC_GIT_SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts 2>/dev/null
|
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
@@ -79,23 +79,23 @@ if [ -n "$CUBBI_PROJECT_URL" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up token if provided
|
# Set up token if provided
|
||||||
if [ -n "$CUBBI_GIT_TOKEN" ]; then
|
if [ -n "$MC_GIT_TOKEN" ]; then
|
||||||
git config --global credential.helper store
|
git config --global credential.helper store
|
||||||
echo "https://$CUBBI_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
|
echo "https://$MC_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clone repository
|
# Clone repository
|
||||||
git clone $CUBBI_PROJECT_URL /app
|
git clone $MC_PROJECT_URL /app
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
# Run project-specific initialization if present
|
# Run project-specific initialization if present
|
||||||
if [ -f "/app/.cubbi/init.sh" ]; then
|
if [ -f "/app/.mc/init.sh" ]; then
|
||||||
bash /app/.cubbi/init.sh
|
bash /app/.mc/init.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Persistent configs are now directly mounted as volumes
|
# Persistent configs are now directly mounted as volumes
|
||||||
# No need to create symlinks anymore
|
# No need to create symlinks anymore
|
||||||
if [ -n "$CUBBI_CONFIG_DIR" ] && [ -d "$CUBBI_CONFIG_DIR" ]; then
|
if [ -n "$MC_CONFIG_DIR" ] && [ -d "$MC_CONFIG_DIR" ]; then
|
||||||
echo "Using persistent configuration volumes (direct mounts)"
|
echo "Using persistent configuration volumes (direct mounts)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -110,18 +110,18 @@ if [ -n "$LANGFUSE_INIT_PROJECT_SECRET_KEY" ] && [ -n "$LANGFUSE_INIT_PROJECT_PU
|
|||||||
export LANGFUSE_URL="${LANGFUSE_URL:-https://cloud.langfuse.com}"
|
export LANGFUSE_URL="${LANGFUSE_URL:-https://cloud.langfuse.com}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure /cubbi-config directory exists (required for symlinks)
|
# Ensure /mc-config directory exists (required for symlinks)
|
||||||
if [ ! -d "/cubbi-config" ]; then
|
if [ ! -d "/mc-config" ]; then
|
||||||
echo "Creating /cubbi-config directory since it doesn't exist"
|
echo "Creating /mc-config directory since it doesn't exist"
|
||||||
mkdir -p /cubbi-config
|
mkdir -p /mc-config
|
||||||
chown $CUBBI_USER_ID:$CUBBI_GROUP_ID /cubbi-config
|
chown $MC_USER_ID:$MC_GROUP_ID /mc-config
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create symlinks for persistent configurations defined in the image
|
# Create symlinks for persistent configurations defined in the driver
|
||||||
if [ -n "$CUBBI_PERSISTENT_LINKS" ]; then
|
if [ -n "$MC_PERSISTENT_LINKS" ]; then
|
||||||
echo "Creating persistent configuration symlinks..."
|
echo "Creating persistent configuration symlinks..."
|
||||||
# Split by semicolon
|
# Split by semicolon
|
||||||
IFS=';' read -ra LINKS <<< "$CUBBI_PERSISTENT_LINKS"
|
IFS=';' read -ra LINKS <<< "$MC_PERSISTENT_LINKS"
|
||||||
for link_pair in "${LINKS[@]}"; do
|
for link_pair in "${LINKS[@]}"; do
|
||||||
# Split by colon
|
# Split by colon
|
||||||
IFS=':' read -r source_path target_path <<< "$link_pair"
|
IFS=':' read -r source_path target_path <<< "$link_pair"
|
||||||
@@ -134,46 +134,47 @@ if [ -n "$CUBBI_PERSISTENT_LINKS" ]; then
|
|||||||
echo "Processing link: $source_path -> $target_path"
|
echo "Processing link: $source_path -> $target_path"
|
||||||
parent_dir=$(dirname "$source_path")
|
parent_dir=$(dirname "$source_path")
|
||||||
|
|
||||||
# Ensure parent directory of the link source exists and is owned by cubbi
|
# Ensure parent directory of the link source exists and is owned by mcuser
|
||||||
if [ ! -d "$parent_dir" ]; then
|
if [ ! -d "$parent_dir" ]; then
|
||||||
echo "Creating parent directory: $parent_dir"
|
echo "Creating parent directory: $parent_dir"
|
||||||
mkdir -p "$parent_dir"
|
mkdir -p "$parent_dir"
|
||||||
echo "Changing ownership of parent $parent_dir to $CUBBI_USER_ID:$CUBBI_GROUP_ID"
|
echo "Changing ownership of parent $parent_dir to $MC_USER_ID:$MC_GROUP_ID"
|
||||||
chown "$CUBBI_USER_ID:$CUBBI_GROUP_ID" "$parent_dir" || echo "Warning: Could not chown parent $parent_dir"
|
chown "$MC_USER_ID:$MC_GROUP_ID" "$parent_dir" || echo "Warning: Could not chown parent $parent_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the symlink (force, no-dereference)
|
# Create the symlink (force, no-dereference)
|
||||||
echo "Creating symlink: ln -sfn $target_path $source_path"
|
echo "Creating symlink: ln -sfn $target_path $source_path"
|
||||||
ln -sfn "$target_path" "$source_path"
|
ln -sfn "$target_path" "$source_path"
|
||||||
|
|
||||||
# Optionally, change ownership of the symlink itself
|
# Optionally, change ownership of the symlink itself
|
||||||
echo "Changing ownership of symlink $source_path to $CUBBI_USER_ID:$CUBBI_GROUP_ID"
|
echo "Changing ownership of symlink $source_path to $MC_USER_ID:$MC_GROUP_ID"
|
||||||
chown -h "$CUBBI_USER_ID:$CUBBI_GROUP_ID" "$source_path" || echo "Warning: Could not chown symlink $source_path"
|
chown -h "$MC_USER_ID:$MC_GROUP_ID" "$source_path" || echo "Warning: Could not chown symlink $source_path"
|
||||||
|
|
||||||
done
|
done
|
||||||
echo "Persistent configuration symlinks created."
|
echo "Persistent configuration symlinks created."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update Goose configuration with available MCP servers (run as cubbi after symlinks are created)
|
# Update Goose configuration with available MCP servers (run as mcuser after symlinks are created)
|
||||||
if [ -f "/usr/local/bin/update-goose-config.py" ]; then
|
if [ -f "/usr/local/bin/update-goose-config.py" ]; then
|
||||||
echo "Updating Goose configuration with MCP servers as cubbi..."
|
echo "Updating Goose configuration with MCP servers as mcuser..."
|
||||||
gosu cubbi /usr/local/bin/update-goose-config.py
|
gosu mcuser /usr/local/bin/update-goose-config.py
|
||||||
elif [ -f "$(dirname "$0")/update-goose-config.py" ]; then
|
elif [ -f "$(dirname "$0")/update-goose-config.py" ]; then
|
||||||
echo "Updating Goose configuration with MCP servers as cubbi..."
|
echo "Updating Goose configuration with MCP servers as mcuser..."
|
||||||
gosu cubbi "$(dirname "$0")/update-goose-config.py"
|
gosu mcuser "$(dirname "$0")/update-goose-config.py"
|
||||||
else
|
else
|
||||||
echo "Warning: update-goose-config.py script not found. Goose configuration will not be updated."
|
echo "Warning: update-goose-config.py script not found. Goose configuration will not be updated."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the user command first, if set, as cubbi
|
# Run the user command first, if set, as mcuser
|
||||||
if [ -n "$CUBBI_RUN_COMMAND" ]; then
|
if [ -n "$MC_RUN_COMMAND" ]; then
|
||||||
echo "--- Executing initial command: $CUBBI_RUN_COMMAND ---";
|
echo "--- Executing initial command: $MC_RUN_COMMAND ---";
|
||||||
gosu cubbi sh -c "$CUBBI_RUN_COMMAND"; # Run user command as cubbi
|
gosu mcuser sh -c "$MC_RUN_COMMAND"; # Run user command as mcuser
|
||||||
COMMAND_EXIT_CODE=$?;
|
COMMAND_EXIT_CODE=$?;
|
||||||
echo "--- Initial command finished (exit code: $COMMAND_EXIT_CODE) ---";
|
echo "--- Initial command finished (exit code: $COMMAND_EXIT_CODE) ---";
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
# Mark initialization as complete
|
# Mark initialization as complete
|
||||||
echo "=== Cubbi Initialization completed at $(date) ==="
|
echo "=== MC Initialization completed at $(date) ==="
|
||||||
echo "INIT_COMPLETE=true" > /init.status
|
echo "INIT_COMPLETE=true" > /init.status
|
||||||
|
|
||||||
exec gosu cubbi "$@"
|
exec gosu mcuser "$@"
|
||||||
@@ -39,8 +39,8 @@ def update_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Update goose configuration with model and provider from environment variables
|
# Update goose configuration with model and provider from environment variables
|
||||||
goose_model = os.environ.get("CUBBI_MODEL")
|
goose_model = os.environ.get("MC_MODEL")
|
||||||
goose_provider = os.environ.get("CUBBI_PROVIDER")
|
goose_provider = os.environ.get("MC_PROVIDER")
|
||||||
|
|
||||||
if goose_model:
|
if goose_model:
|
||||||
config_data["GOOSE_MODEL"] = goose_model
|
config_data["GOOSE_MODEL"] = goose_model
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
MCP (Model Control Protocol) server management for Cubbi Container.
|
MCP (Model Control Protocol) server management for Monadical Container.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
from docker.errors import DockerException, ImageNotFound, NotFound
|
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||||
|
|
||||||
from .models import DockerMCP, MCPContainer, MCPStatus, ProxyMCP, RemoteMCP
|
from .models import MCPStatus, RemoteMCP, DockerMCP, ProxyMCP, MCPContainer
|
||||||
from .user_config import UserConfigManager
|
from .user_config import UserConfigManager
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -38,7 +37,7 @@ class MCPManager:
|
|||||||
"""Ensure the MCP network exists and return its name.
|
"""Ensure the MCP network exists and return its name.
|
||||||
Note: This is used only by the inspector, not for session-to-MCP connections.
|
Note: This is used only by the inspector, not for session-to-MCP connections.
|
||||||
"""
|
"""
|
||||||
network_name = "cubbi-mcp-network"
|
network_name = "mc-mcp-network"
|
||||||
if self.client:
|
if self.client:
|
||||||
networks = self.client.networks.list(names=[network_name])
|
networks = self.client.networks.list(names=[network_name])
|
||||||
if not networks:
|
if not networks:
|
||||||
@@ -54,7 +53,7 @@ class MCPManager:
|
|||||||
Returns:
|
Returns:
|
||||||
The name of the dedicated network
|
The name of the dedicated network
|
||||||
"""
|
"""
|
||||||
network_name = f"cubbi-mcp-{mcp_name}-network"
|
network_name = f"mc-mcp-{mcp_name}-network"
|
||||||
if self.client:
|
if self.client:
|
||||||
networks = self.client.networks.list(names=[network_name])
|
networks = self.client.networks.list(names=[network_name])
|
||||||
if not networks:
|
if not networks:
|
||||||
@@ -282,7 +281,7 @@ class MCPManager:
|
|||||||
|
|
||||||
def get_mcp_container_name(self, mcp_name: str) -> str:
|
def get_mcp_container_name(self, mcp_name: str) -> str:
|
||||||
"""Get the Docker container name for an MCP server."""
|
"""Get the Docker container name for an MCP server."""
|
||||||
return f"cubbi_mcp_{mcp_name}"
|
return f"mc_mcp_{mcp_name}"
|
||||||
|
|
||||||
def start_mcp(self, name: str) -> Dict[str, Any]:
|
def start_mcp(self, name: str) -> Dict[str, Any]:
|
||||||
"""Start an MCP server container."""
|
"""Start an MCP server container."""
|
||||||
@@ -374,9 +373,9 @@ class MCPManager:
|
|||||||
network=None, # Start without network, we'll add it with aliases
|
network=None, # Start without network, we'll add it with aliases
|
||||||
environment=mcp_config.get("env", {}),
|
environment=mcp_config.get("env", {}),
|
||||||
labels={
|
labels={
|
||||||
"cubbi.mcp": "true",
|
"mc.mcp": "true",
|
||||||
"cubbi.mcp.name": name,
|
"mc.mcp.name": name,
|
||||||
"cubbi.mcp.type": "docker",
|
"mc.mcp.type": "docker",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -540,7 +539,7 @@ ENTRYPOINT ["/entrypoint.sh"]
|
|||||||
f.write(dockerfile_content)
|
f.write(dockerfile_content)
|
||||||
|
|
||||||
# Build the image
|
# Build the image
|
||||||
custom_image_name = f"cubbi_mcp_proxy_{name}"
|
custom_image_name = f"mc_mcp_proxy_{name}"
|
||||||
logger.info(f"Building custom proxy image: {custom_image_name}")
|
logger.info(f"Building custom proxy image: {custom_image_name}")
|
||||||
self.client.images.build(
|
self.client.images.build(
|
||||||
path=tmp_dir,
|
path=tmp_dir,
|
||||||
@@ -577,9 +576,9 @@ ENTRYPOINT ["/entrypoint.sh"]
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
labels={
|
labels={
|
||||||
"cubbi.mcp": "true",
|
"mc.mcp": "true",
|
||||||
"cubbi.mcp.name": name,
|
"mc.mcp.name": name,
|
||||||
"cubbi.mcp.type": "proxy",
|
"mc.mcp.type": "proxy",
|
||||||
},
|
},
|
||||||
ports=port_bindings, # Bind the SSE port to the host if configured
|
ports=port_bindings, # Bind the SSE port to the host if configured
|
||||||
)
|
)
|
||||||
@@ -816,10 +815,8 @@ ENTRYPOINT ["/entrypoint.sh"]
|
|||||||
if not self.client:
|
if not self.client:
|
||||||
raise Exception("Docker client is not available")
|
raise Exception("Docker client is not available")
|
||||||
|
|
||||||
# Get all containers with the cubbi.mcp label
|
# Get all containers with the mc.mcp label
|
||||||
containers = self.client.containers.list(
|
containers = self.client.containers.list(all=True, filters={"label": "mc.mcp"})
|
||||||
all=True, filters={"label": "cubbi.mcp"}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for container in containers:
|
for container in containers:
|
||||||
@@ -860,13 +857,13 @@ ENTRYPOINT ["/entrypoint.sh"]
|
|||||||
|
|
||||||
# Create MCPContainer object
|
# Create MCPContainer object
|
||||||
mcp_container = MCPContainer(
|
mcp_container = MCPContainer(
|
||||||
name=labels.get("cubbi.mcp.name", "unknown"),
|
name=labels.get("mc.mcp.name", "unknown"),
|
||||||
container_id=container.id,
|
container_id=container.id,
|
||||||
status=status,
|
status=status,
|
||||||
image=container_info["Config"]["Image"],
|
image=container_info["Config"]["Image"],
|
||||||
ports=ports,
|
ports=ports,
|
||||||
created_at=container_info["Created"],
|
created_at=container_info["Created"],
|
||||||
type=labels.get("cubbi.mcp.type", "unknown"),
|
type=labels.get("mc.mcp.type", "unknown"),
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(mcp_container)
|
result.append(mcp_container)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union, Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ class MCPStatus(str, Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
class ImageEnvironmentVariable(BaseModel):
|
class DriverEnvironmentVariable(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
required: bool = False
|
required: bool = False
|
||||||
@@ -38,19 +37,19 @@ class VolumeMount(BaseModel):
|
|||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ImageInit(BaseModel):
|
class DriverInit(BaseModel):
|
||||||
pre_command: Optional[str] = None
|
pre_command: Optional[str] = None
|
||||||
command: str
|
command: str
|
||||||
|
|
||||||
|
|
||||||
class Image(BaseModel):
|
class Driver(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
version: str
|
version: str
|
||||||
maintainer: str
|
maintainer: str
|
||||||
image: str
|
image: str
|
||||||
init: Optional[ImageInit] = None
|
init: Optional[DriverInit] = None
|
||||||
environment: List[ImageEnvironmentVariable] = []
|
environment: List[DriverEnvironmentVariable] = []
|
||||||
ports: List[int] = []
|
ports: List[int] = []
|
||||||
volumes: List[VolumeMount] = []
|
volumes: List[VolumeMount] = []
|
||||||
persistent_configs: List[PersistentConfig] = []
|
persistent_configs: List[PersistentConfig] = []
|
||||||
@@ -98,7 +97,7 @@ class MCPContainer(BaseModel):
|
|||||||
class Session(BaseModel):
|
class Session(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
image: str
|
driver: str
|
||||||
status: SessionStatus
|
status: SessionStatus
|
||||||
container_id: Optional[str] = None
|
container_id: Optional[str] = None
|
||||||
ports: Dict[int, int] = Field(default_factory=dict)
|
ports: Dict[int, int] = Field(default_factory=dict)
|
||||||
@@ -106,7 +105,7 @@ class Session(BaseModel):
|
|||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
docker: Dict[str, str] = Field(default_factory=dict)
|
docker: Dict[str, str] = Field(default_factory=dict)
|
||||||
images: Dict[str, Image] = Field(default_factory=dict)
|
drivers: Dict[str, Driver] = Field(default_factory=dict)
|
||||||
defaults: Dict[str, object] = Field(
|
defaults: Dict[str, object] = Field(
|
||||||
default_factory=dict
|
default_factory=dict
|
||||||
) # Can store strings, booleans, or other values
|
) # Can store strings, booleans, or other values
|
||||||
14
mcontainer/service.py
Normal file
14
mcontainer/service.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
MC Service - Container Management Web Service
|
||||||
|
(This is a placeholder for Phase 2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the MC service"""
|
||||||
|
print("MC Service - Container Management Web Service")
|
||||||
|
print("This feature will be implemented in Phase 2")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Session storage management for Cubbi Container Tool.
|
Session storage management for Monadical Container Tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import yaml
|
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "mc" / "sessions.yaml"
|
||||||
|
|
||||||
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "cubbi" / "sessions.yaml"
|
|
||||||
|
|
||||||
|
|
||||||
class SessionManager:
|
class SessionManager:
|
||||||
@@ -19,7 +18,7 @@ class SessionManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
sessions_path: Optional path to the sessions file.
|
sessions_path: Optional path to the sessions file.
|
||||||
Defaults to ~/.config/cubbi/sessions.yaml.
|
Defaults to ~/.config/mc/sessions.yaml.
|
||||||
"""
|
"""
|
||||||
self.sessions_path = sessions_path or DEFAULT_SESSIONS_FILE
|
self.sessions_path = sessions_path or DEFAULT_SESSIONS_FILE
|
||||||
self.sessions = self._load_sessions()
|
self.sessions = self._load_sessions()
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
User configuration manager for Cubbi Container Tool.
|
User configuration manager for Monadical Container Tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional, List, Tuple
|
||||||
|
|
||||||
# Define the environment variable mappings
|
# Define the environment variable mappings
|
||||||
ENV_MAPPINGS = {
|
ENV_MAPPINGS = {
|
||||||
@@ -28,11 +27,11 @@ class UserConfigManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path: Optional path to the configuration file.
|
config_path: Optional path to the configuration file.
|
||||||
Defaults to ~/.config/cubbi/config.yaml.
|
Defaults to ~/.config/mc/config.yaml.
|
||||||
"""
|
"""
|
||||||
# Default to ~/.config/cubbi/config.yaml
|
# Default to ~/.config/mc/config.yaml
|
||||||
self.config_path = Path(
|
self.config_path = Path(
|
||||||
config_path or os.path.expanduser("~/.config/cubbi/config.yaml")
|
config_path or os.path.expanduser("~/.config/mc/config.yaml")
|
||||||
)
|
)
|
||||||
self.config = self._load_config()
|
self.config = self._load_config()
|
||||||
|
|
||||||
@@ -90,10 +89,10 @@ class UserConfigManager:
|
|||||||
"""Get the default configuration."""
|
"""Get the default configuration."""
|
||||||
return {
|
return {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"image": "goose",
|
"driver": "goose",
|
||||||
"connect": True,
|
"connect": True,
|
||||||
"mount_local": True,
|
"mount_local": True,
|
||||||
"networks": [], # Default networks to connect to (besides cubbi-network)
|
"networks": [], # Default networks to connect to (besides mc-network)
|
||||||
"volumes": [], # Default volumes to mount, format: "source:dest"
|
"volumes": [], # Default volumes to mount, format: "source:dest"
|
||||||
"mcps": [], # Default MCP servers to connect to
|
"mcps": [], # Default MCP servers to connect to
|
||||||
"model": "claude-3-5-sonnet-latest", # Default LLM model to use
|
"model": "claude-3-5-sonnet-latest", # Default LLM model to use
|
||||||
@@ -107,7 +106,7 @@ class UserConfigManager:
|
|||||||
"google": {},
|
"google": {},
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"network": "cubbi-network",
|
"network": "mc-network",
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"colors": True,
|
"colors": True,
|
||||||
@@ -134,7 +133,7 @@ class UserConfigManager:
|
|||||||
"""Get a configuration value by dot-notation path.
|
"""Get a configuration value by dot-notation path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key_path: The configuration path (e.g., "defaults.image")
|
key_path: The configuration path (e.g., "defaults.driver")
|
||||||
default: The default value to return if not found
|
default: The default value to return if not found
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -166,7 +165,7 @@ class UserConfigManager:
|
|||||||
"""Set a configuration value by dot-notation path.
|
"""Set a configuration value by dot-notation path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key_path: The configuration path (e.g., "defaults.image")
|
key_path: The configuration path (e.g., "defaults.driver")
|
||||||
value: The value to set
|
value: The value to set
|
||||||
"""
|
"""
|
||||||
# Handle shorthand service paths (e.g., "langfuse.url")
|
# Handle shorthand service paths (e.g., "langfuse.url")
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "cubbi"
|
name = "mcontainer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Cubbi Container Tool"
|
description = "Monadical Container Tool"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -24,8 +24,8 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
cubbi = "cubbi.cli:app"
|
mc = "mcontainer.cli:app"
|
||||||
cubbix = "cubbi.cli:session_create_entry_point"
|
mcx = "mcontainer.cli:session_create_entry_point"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Common test fixtures for Cubbi Container tests.
|
Common test fixtures for Monadical Container tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
@@ -9,11 +9,11 @@ import docker
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from cubbi.container import ContainerManager
|
from mcontainer.container import ContainerManager
|
||||||
from cubbi.session import SessionManager
|
from mcontainer.session import SessionManager
|
||||||
from cubbi.config import ConfigManager
|
from mcontainer.config import ConfigManager
|
||||||
from cubbi.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
from cubbi.user_config import UserConfigManager
|
from mcontainer.user_config import UserConfigManager
|
||||||
|
|
||||||
|
|
||||||
# Check if Docker is available
|
# Check if Docker is available
|
||||||
@@ -74,14 +74,14 @@ def isolated_session_manager(temp_config_dir):
|
|||||||
def isolated_config_manager():
|
def isolated_config_manager():
|
||||||
"""Create an isolated config manager for testing."""
|
"""Create an isolated config manager for testing."""
|
||||||
config_manager = ConfigManager()
|
config_manager = ConfigManager()
|
||||||
# Ensure we're using the built-in images, not trying to load from user config
|
# Ensure we're using the built-in drivers, not trying to load from user config
|
||||||
return config_manager
|
return config_manager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_session_manager():
|
def mock_session_manager():
|
||||||
"""Mock the SessionManager class."""
|
"""Mock the SessionManager class."""
|
||||||
with patch("cubbi.cli.session_manager") as mock_manager:
|
with patch("mcontainer.cli.session_manager") as mock_manager:
|
||||||
yield mock_manager
|
yield mock_manager
|
||||||
|
|
||||||
|
|
||||||
@@ -91,12 +91,12 @@ def mock_container_manager():
|
|||||||
mock_session = Session(
|
mock_session = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
image="goose",
|
driver="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={"8080": "8080"},
|
ports={"8080": "8080"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("cubbi.cli.container_manager") as mock_manager:
|
with patch("mcontainer.cli.container_manager") as mock_manager:
|
||||||
# Set behaviors to avoid TypeErrors
|
# Set behaviors to avoid TypeErrors
|
||||||
mock_manager.list_sessions.return_value = []
|
mock_manager.list_sessions.return_value = []
|
||||||
mock_manager.create_session.return_value = mock_session
|
mock_manager.create_session.return_value = mock_session
|
||||||
@@ -149,7 +149,7 @@ def test_file_content(temp_dir):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_network_name():
|
def test_network_name():
|
||||||
"""Generate a unique network name for testing."""
|
"""Generate a unique network name for testing."""
|
||||||
return f"cubbi-test-network-{uuid.uuid4().hex[:8]}"
|
return f"mc-test-network-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -175,5 +175,5 @@ def docker_test_network(test_network_name):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patched_config_manager(isolated_config):
|
def patched_config_manager(isolated_config):
|
||||||
"""Patch the UserConfigManager in cli.py to use our isolated instance."""
|
"""Patch the UserConfigManager in cli.py to use our isolated instance."""
|
||||||
with patch("cubbi.cli.user_config", isolated_config):
|
with patch("mcontainer.cli.user_config", isolated_config):
|
||||||
yield isolated_config
|
yield isolated_config
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from cubbi.cli import app
|
from mcontainer.cli import app
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ def test_version() -> None:
|
|||||||
"""Test version command"""
|
"""Test version command"""
|
||||||
result = runner.invoke(app, ["version"])
|
result = runner.invoke(app, ["version"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Cubbi - Cubbi Container Tool" in result.stdout
|
assert "MC - Monadical Container Tool" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_session_list() -> None:
|
def test_session_list() -> None:
|
||||||
@@ -25,4 +25,4 @@ def test_help() -> None:
|
|||||||
result = runner.invoke(app, ["--help"])
|
result = runner.invoke(app, ["--help"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Usage" in result.stdout
|
assert "Usage" in result.stdout
|
||||||
assert "Cubbi Container Tool" in result.stdout
|
assert "Monadical Container Tool" in result.stdout
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
Tests for the configuration management commands.
|
Tests for the configuration management commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cubbi.cli import app
|
from mcontainer.cli import app
|
||||||
|
|
||||||
|
|
||||||
def test_config_list(cli_runner, patched_config_manager):
|
def test_config_list(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi config list' command."""
|
"""Test the 'mc config list' command."""
|
||||||
result = cli_runner.invoke(app, ["config", "list"])
|
result = cli_runner.invoke(app, ["config", "list"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
@@ -14,18 +14,18 @@ def test_config_list(cli_runner, patched_config_manager):
|
|||||||
assert "Value" in result.stdout
|
assert "Value" in result.stdout
|
||||||
|
|
||||||
# Check for default configurations
|
# Check for default configurations
|
||||||
assert "defaults.image" in result.stdout
|
assert "defaults.driver" in result.stdout
|
||||||
assert "defaults.connect" in result.stdout
|
assert "defaults.connect" in result.stdout
|
||||||
assert "defaults.mount_local" in result.stdout
|
assert "defaults.mount_local" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_config_get(cli_runner, patched_config_manager):
|
def test_config_get(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi config get' command."""
|
"""Test the 'mc config get' command."""
|
||||||
# Test getting an existing value
|
# Test getting an existing value
|
||||||
result = cli_runner.invoke(app, ["config", "get", "defaults.image"])
|
result = cli_runner.invoke(app, ["config", "get", "defaults.driver"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "defaults.image" in result.stdout
|
assert "defaults.driver" in result.stdout
|
||||||
assert "goose" in result.stdout
|
assert "goose" in result.stdout
|
||||||
|
|
||||||
# Test getting a non-existent value
|
# Test getting a non-existent value
|
||||||
@@ -36,13 +36,13 @@ def test_config_get(cli_runner, patched_config_manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_config_set(cli_runner, patched_config_manager):
|
def test_config_set(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi config set' command."""
|
"""Test the 'mc config set' command."""
|
||||||
# Test setting a string value
|
# Test setting a string value
|
||||||
result = cli_runner.invoke(app, ["config", "set", "defaults.image", "claude"])
|
result = cli_runner.invoke(app, ["config", "set", "defaults.driver", "claude"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Configuration updated" in result.stdout
|
assert "Configuration updated" in result.stdout
|
||||||
assert patched_config_manager.get("defaults.image") == "claude"
|
assert patched_config_manager.get("defaults.driver") == "claude"
|
||||||
|
|
||||||
# Test setting a boolean value
|
# Test setting a boolean value
|
||||||
result = cli_runner.invoke(app, ["config", "set", "defaults.connect", "false"])
|
result = cli_runner.invoke(app, ["config", "set", "defaults.connect", "false"])
|
||||||
@@ -60,7 +60,7 @@ def test_config_set(cli_runner, patched_config_manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_volume_list_empty(cli_runner, patched_config_manager):
|
def test_volume_list_empty(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi config volume list' command with no volumes."""
|
"""Test the 'mc config volume list' command with no volumes."""
|
||||||
result = cli_runner.invoke(app, ["config", "volume", "list"])
|
result = cli_runner.invoke(app, ["config", "volume", "list"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
@@ -81,13 +81,11 @@ def test_volume_add_and_list(cli_runner, patched_config_manager, temp_config_dir
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Added volume" in result.stdout
|
assert "Added volume" in result.stdout
|
||||||
|
|
||||||
# Verify volume was added to the configuration
|
# List volumes
|
||||||
volumes = patched_config_manager.get("defaults.volumes", [])
|
|
||||||
assert f"{test_dir}:/container/path" in volumes
|
|
||||||
|
|
||||||
# List volumes - just check the command runs without error
|
|
||||||
result = cli_runner.invoke(app, ["config", "volume", "list"])
|
result = cli_runner.invoke(app, ["config", "volume", "list"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
assert str(test_dir) in result.stdout
|
||||||
assert "/container/path" in result.stdout
|
assert "/container/path" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
@@ -133,7 +131,7 @@ def test_volume_add_nonexistent_path(cli_runner, patched_config_manager, monkeyp
|
|||||||
|
|
||||||
|
|
||||||
def test_network_list_empty(cli_runner, patched_config_manager):
|
def test_network_list_empty(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi config network list' command with no networks."""
|
"""Test the 'mc config network list' command with no networks."""
|
||||||
result = cli_runner.invoke(app, ["config", "network", "list"])
|
result = cli_runner.invoke(app, ["config", "network", "list"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
@@ -174,7 +172,7 @@ def test_network_remove(cli_runner, patched_config_manager):
|
|||||||
def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
||||||
"""Test resetting the configuration."""
|
"""Test resetting the configuration."""
|
||||||
# Set a custom value first
|
# Set a custom value first
|
||||||
patched_config_manager.set("defaults.image", "custom-image")
|
patched_config_manager.set("defaults.driver", "custom-driver")
|
||||||
|
|
||||||
# Mock typer.confirm to return True
|
# Mock typer.confirm to return True
|
||||||
monkeypatch.setattr("typer.confirm", lambda message: True)
|
monkeypatch.setattr("typer.confirm", lambda message: True)
|
||||||
@@ -186,7 +184,7 @@ def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
|||||||
assert "Configuration reset to defaults" in result.stdout
|
assert "Configuration reset to defaults" in result.stdout
|
||||||
|
|
||||||
# Verify it was reset
|
# Verify it was reset
|
||||||
assert patched_config_manager.get("defaults.image") == "goose"
|
assert patched_config_manager.get("defaults.driver") == "goose"
|
||||||
|
|
||||||
|
|
||||||
# patched_config_manager fixture is now in conftest.py
|
# patched_config_manager fixture is now in conftest.py
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Integration tests for Docker interactions in Cubbi Container.
|
Integration tests for Docker interactions in Monadical Container.
|
||||||
These tests require Docker to be running.
|
These tests require Docker to be running.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ def test_integration_session_create_with_volumes(container_manager, test_file_co
|
|||||||
try:
|
try:
|
||||||
# Create a session with a volume mount
|
# Create a session with a volume mount
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
image_name="goose",
|
driver_name="goose",
|
||||||
session_name=f"cubbi-test-volume-{uuid.uuid4().hex[:8]}",
|
session_name=f"mc-test-volume-{uuid.uuid4().hex[:8]}",
|
||||||
mount_local=False, # Don't mount current directory
|
mount_local=False, # Don't mount current directory
|
||||||
volumes={str(test_file): {"bind": "/test/volume_test.txt", "mode": "ro"}},
|
volumes={str(test_file): {"bind": "/test/volume_test.txt", "mode": "ro"}},
|
||||||
)
|
)
|
||||||
@@ -65,8 +65,8 @@ def test_integration_session_create_with_networks(
|
|||||||
try:
|
try:
|
||||||
# Create a session with the test network
|
# Create a session with the test network
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
image_name="goose",
|
driver_name="goose",
|
||||||
session_name=f"cubbi-test-network-{uuid.uuid4().hex[:8]}",
|
session_name=f"mc-test-network-{uuid.uuid4().hex[:8]}",
|
||||||
mount_local=False, # Don't mount current directory
|
mount_local=False, # Don't mount current directory
|
||||||
networks=[docker_test_network],
|
networks=[docker_test_network],
|
||||||
)
|
)
|
||||||
@@ -85,7 +85,7 @@ def test_integration_session_create_with_networks(
|
|||||||
container = client.containers.get(session.container_id)
|
container = client.containers.get(session.container_id)
|
||||||
container_networks = container.attrs["NetworkSettings"]["Networks"]
|
container_networks = container.attrs["NetworkSettings"]["Networks"]
|
||||||
|
|
||||||
# Container should be connected to both the default cubbi-network and our test network
|
# Container should be connected to both the default mc-network and our test network
|
||||||
assert docker_test_network in container_networks
|
assert docker_test_network in container_networks
|
||||||
|
|
||||||
# Verify network interface exists in container
|
# Verify network interface exists in container
|
||||||
@@ -93,7 +93,7 @@ def test_integration_session_create_with_networks(
|
|||||||
session.container_id, "ip link show | grep -v 'lo' | wc -l"
|
session.container_id, "ip link show | grep -v 'lo' | wc -l"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should have at least 2 interfaces (eth0 for cubbi-network, eth1 for test network)
|
# Should have at least 2 interfaces (eth0 for mc-network, eth1 for test network)
|
||||||
assert int(network_interfaces) >= 2
|
assert int(network_interfaces) >= 2
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ Tests for the MCP server management commands.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from cubbi.cli import app
|
from mcontainer.cli import app
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_list_empty(cli_runner, patched_config_manager):
|
def test_mcp_list_empty(cli_runner, patched_config_manager):
|
||||||
"""Test the 'cubbi mcp list' command with no MCPs configured."""
|
"""Test the 'mc mcp list' command with no MCPs configured."""
|
||||||
# Make sure mcps is empty
|
# Make sure mcps is empty
|
||||||
patched_config_manager.set("mcps", [])
|
patched_config_manager.set("mcps", [])
|
||||||
|
|
||||||
with patch("cubbi.cli.mcp_manager.list_mcps") as mock_list_mcps:
|
with patch("mcontainer.cli.mcp_manager.list_mcps") as mock_list_mcps:
|
||||||
mock_list_mcps.return_value = []
|
mock_list_mcps.return_value = []
|
||||||
|
|
||||||
result = cli_runner.invoke(app, ["mcp", "list"])
|
result = cli_runner.invoke(app, ["mcp", "list"])
|
||||||
@@ -21,14 +21,15 @@ def test_mcp_list_empty(cli_runner, patched_config_manager):
|
|||||||
assert "No MCP servers configured" in result.stdout
|
assert "No MCP servers configured" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_add_remote(cli_runner, patched_config_manager):
|
def test_mcp_remote_add_and_list(cli_runner, patched_config_manager):
|
||||||
"""Test adding a remote MCP server and listing it."""
|
"""Test adding a remote MCP server and listing it."""
|
||||||
# Add a remote MCP server
|
# Add a remote MCP server
|
||||||
result = cli_runner.invoke(
|
result = cli_runner.invoke(
|
||||||
app,
|
app,
|
||||||
[
|
[
|
||||||
"mcp",
|
"mcp",
|
||||||
"add-remote",
|
"remote",
|
||||||
|
"add",
|
||||||
"test-remote-mcp",
|
"test-remote-mcp",
|
||||||
"http://mcp-server.example.com/sse",
|
"http://mcp-server.example.com/sse",
|
||||||
"--header",
|
"--header",
|
||||||
@@ -46,16 +47,17 @@ def test_mcp_add_remote(cli_runner, patched_config_manager):
|
|||||||
assert "test-remote-mcp" in result.stdout
|
assert "test-remote-mcp" in result.stdout
|
||||||
assert "remote" in result.stdout
|
assert "remote" in result.stdout
|
||||||
# Check partial URL since it may be truncated in the table display
|
# Check partial URL since it may be truncated in the table display
|
||||||
assert "http://mcp-se" in result.stdout # Truncated in table view
|
assert "http://mcp-server.example.com" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_add(cli_runner, patched_config_manager):
|
def test_mcp_docker_add_and_list(cli_runner, patched_config_manager):
|
||||||
"""Test adding a proxy-based MCP server and listing it."""
|
"""Test adding a Docker-based MCP server and listing it."""
|
||||||
# Add a Docker MCP server
|
# Add a Docker MCP server
|
||||||
result = cli_runner.invoke(
|
result = cli_runner.invoke(
|
||||||
app,
|
app,
|
||||||
[
|
[
|
||||||
"mcp",
|
"mcp",
|
||||||
|
"docker",
|
||||||
"add",
|
"add",
|
||||||
"test-docker-mcp",
|
"test-docker-mcp",
|
||||||
"mcp/github:latest",
|
"mcp/github:latest",
|
||||||
@@ -67,15 +69,58 @@ def test_mcp_add(cli_runner, patched_config_manager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Added MCP server" in result.stdout
|
assert "Added Docker-based MCP server" in result.stdout
|
||||||
|
|
||||||
# List MCP servers
|
# List MCP servers
|
||||||
result = cli_runner.invoke(app, ["mcp", "list"])
|
result = cli_runner.invoke(app, ["mcp", "list"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "test-docker-mcp" in result.stdout
|
assert "test-docker-mcp" in result.stdout
|
||||||
assert "proxy" in result.stdout # It's a proxy-based MCP
|
assert "docker" in result.stdout
|
||||||
assert "mcp/github:la" in result.stdout # Truncated in table view
|
assert "mcp/github:latest" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_proxy_add_and_list(cli_runner, patched_config_manager):
|
||||||
|
"""Test adding a proxy-based MCP server and listing it."""
|
||||||
|
# Add a proxy MCP server
|
||||||
|
result = cli_runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
"mcp",
|
||||||
|
"proxy",
|
||||||
|
"add",
|
||||||
|
"test-proxy-mcp",
|
||||||
|
"ghcr.io/mcp/github:latest",
|
||||||
|
"--proxy-image",
|
||||||
|
"ghcr.io/sparfenyuk/mcp-proxy:latest",
|
||||||
|
"--command",
|
||||||
|
"github-mcp",
|
||||||
|
"--sse-port",
|
||||||
|
"8080",
|
||||||
|
"--sse-host",
|
||||||
|
"0.0.0.0",
|
||||||
|
"--allow-origin",
|
||||||
|
"*",
|
||||||
|
"--env",
|
||||||
|
"GITHUB_TOKEN=test-token",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Added proxy-based MCP server" in result.stdout
|
||||||
|
|
||||||
|
# List MCP servers
|
||||||
|
result = cli_runner.invoke(app, ["mcp", "list"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "test-proxy-mcp" in result.stdout
|
||||||
|
assert "proxy" in result.stdout
|
||||||
|
assert (
|
||||||
|
"ghcr.io/mcp/github" in result.stdout
|
||||||
|
) # Partial match due to potential truncation
|
||||||
|
# The proxy image might not be visible in the table output
|
||||||
|
# so we'll check for the specific format we expect instead
|
||||||
|
assert "via" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_remove(cli_runner, patched_config_manager):
|
def test_mcp_remove(cli_runner, patched_config_manager):
|
||||||
@@ -94,7 +139,7 @@ def test_mcp_remove(cli_runner, patched_config_manager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Mock the get_mcp and remove_mcp methods
|
# Mock the get_mcp and remove_mcp methods
|
||||||
with patch("cubbi.cli.mcp_manager.get_mcp") as mock_get_mcp:
|
with patch("mcontainer.cli.mcp_manager.get_mcp") as mock_get_mcp:
|
||||||
# First make get_mcp return our MCP
|
# First make get_mcp return our MCP
|
||||||
mock_get_mcp.return_value = {
|
mock_get_mcp.return_value = {
|
||||||
"name": "test-mcp",
|
"name": "test-mcp",
|
||||||
@@ -103,11 +148,18 @@ def test_mcp_remove(cli_runner, patched_config_manager):
|
|||||||
"headers": {"Authorization": "Bearer test-token"},
|
"headers": {"Authorization": "Bearer test-token"},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove the MCP server
|
# Mock the remove_mcp method to return True
|
||||||
result = cli_runner.invoke(app, ["mcp", "remove", "test-mcp"])
|
with patch("mcontainer.cli.mcp_manager.remove_mcp") as mock_remove_mcp:
|
||||||
|
mock_remove_mcp.return_value = True
|
||||||
|
|
||||||
# Just check it ran successfully with exit code 0
|
# Remove the MCP server
|
||||||
assert result.exit_code == 0
|
result = cli_runner.invoke(app, ["mcp", "remove", "test-mcp"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Removed MCP server" in result.stdout
|
||||||
|
|
||||||
|
# Verify remove_mcp was called with the right name
|
||||||
|
mock_remove_mcp.assert_called_once_with("test-mcp")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires_docker
|
@pytest.mark.requires_docker
|
||||||
@@ -128,7 +180,7 @@ def test_mcp_status(cli_runner, patched_config_manager, mock_container_manager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# First mock get_mcp to return our MCP config
|
# First mock get_mcp to return our MCP config
|
||||||
with patch("cubbi.cli.mcp_manager.get_mcp") as mock_get_mcp:
|
with patch("mcontainer.cli.mcp_manager.get_mcp") as mock_get_mcp:
|
||||||
mock_get_mcp.return_value = {
|
mock_get_mcp.return_value = {
|
||||||
"name": "test-docker-mcp",
|
"name": "test-docker-mcp",
|
||||||
"type": "docker",
|
"type": "docker",
|
||||||
@@ -138,7 +190,7 @@ def test_mcp_status(cli_runner, patched_config_manager, mock_container_manager):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Then mock the get_mcp_status method
|
# Then mock the get_mcp_status method
|
||||||
with patch("cubbi.cli.mcp_manager.get_mcp_status") as mock_get_status:
|
with patch("mcontainer.cli.mcp_manager.get_mcp_status") as mock_get_status:
|
||||||
mock_get_status.return_value = {
|
mock_get_status.return_value = {
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"container_id": "test-container-id",
|
"container_id": "test-container-id",
|
||||||
@@ -211,7 +263,7 @@ def test_mcp_stop(cli_runner, patched_config_manager, mock_container_manager):
|
|||||||
result = cli_runner.invoke(app, ["mcp", "stop", "test-docker-mcp"])
|
result = cli_runner.invoke(app, ["mcp", "stop", "test-docker-mcp"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Stopped and removed MCP server" in result.stdout
|
assert "Stopped MCP server" in result.stdout
|
||||||
assert "test-docker-mcp" in result.stdout
|
assert "test-docker-mcp" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
@@ -262,7 +314,7 @@ def test_mcp_logs(cli_runner, patched_config_manager, mock_container_manager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Mock the logs operation
|
# Mock the logs operation
|
||||||
with patch("cubbi.cli.mcp_manager.get_mcp_logs") as mock_get_logs:
|
with patch("mcontainer.cli.mcp_manager.get_mcp_logs") as mock_get_logs:
|
||||||
mock_get_logs.return_value = "Test log output"
|
mock_get_logs.return_value = "Test log output"
|
||||||
|
|
||||||
# View MCP logs
|
# View MCP logs
|
||||||
@@ -288,16 +340,18 @@ def test_session_with_mcp(cli_runner, patched_config_manager, mock_container_man
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Mock the session creation with MCP
|
# Mock the session creation with MCP
|
||||||
from cubbi.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
|
|
||||||
# timestamp no longer needed since we don't use created_at in Session
|
timestamp = "2023-01-01T00:00:00Z"
|
||||||
mock_container_manager.create_session.return_value = Session(
|
mock_container_manager.create_session.return_value = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
image="goose",
|
driver="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
container_id="test-container-id",
|
container_id="test-container-id",
|
||||||
|
created_at=timestamp,
|
||||||
ports={},
|
ports={},
|
||||||
|
mcps=["test-mcp"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a session with MCP
|
# Create a session with MCP
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from conftest import requires_docker
|
from conftest import requires_docker
|
||||||
from cubbi.mcp import MCPManager
|
from mcontainer.mcp import MCPManager
|
||||||
|
|
||||||
|
|
||||||
@requires_docker
|
@requires_docker
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ Tests for the session management commands.
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
from cubbi.cli import app
|
from mcontainer.cli import app
|
||||||
|
|
||||||
|
|
||||||
def test_session_list_empty(cli_runner, mock_container_manager):
|
def test_session_list_empty(cli_runner, mock_container_manager):
|
||||||
"""Test 'cubbi session list' with no active sessions."""
|
"""Test 'mc session list' with no active sessions."""
|
||||||
mock_container_manager.list_sessions.return_value = []
|
mock_container_manager.list_sessions.return_value = []
|
||||||
|
|
||||||
result = cli_runner.invoke(app, ["session", "list"])
|
result = cli_runner.invoke(app, ["session", "list"])
|
||||||
@@ -19,16 +19,19 @@ def test_session_list_empty(cli_runner, mock_container_manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_session_list_with_sessions(cli_runner, mock_container_manager):
|
def test_session_list_with_sessions(cli_runner, mock_container_manager):
|
||||||
"""Test 'cubbi session list' with active sessions."""
|
"""Test 'mc session list' with active sessions."""
|
||||||
# Create a mock session and set list_sessions to return it
|
# Create a mock session and set list_sessions to return it
|
||||||
from cubbi.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
|
|
||||||
mock_session = Session(
|
mock_session = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
image="goose",
|
driver="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={"8080": "8080"},
|
ports={"8080": "8080"},
|
||||||
|
project=None,
|
||||||
|
created_at="2023-01-01T00:00:00Z",
|
||||||
|
mcps=[],
|
||||||
)
|
)
|
||||||
mock_container_manager.list_sessions.return_value = [mock_session]
|
mock_container_manager.list_sessions.return_value = [mock_session]
|
||||||
|
|
||||||
@@ -40,12 +43,12 @@ def test_session_list_with_sessions(cli_runner, mock_container_manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_session_create_basic(cli_runner, mock_container_manager):
|
def test_session_create_basic(cli_runner, mock_container_manager):
|
||||||
"""Test 'cubbi session create' with basic options."""
|
"""Test 'mc session create' with basic options."""
|
||||||
# We need to patch user_config.get with a side_effect to handle different keys
|
# We need to patch user_config.get with a side_effect to handle different keys
|
||||||
with patch("cubbi.cli.user_config") as mock_user_config:
|
with patch("mcontainer.cli.user_config") as mock_user_config:
|
||||||
# Handle different key requests appropriately
|
# Handle different key requests appropriately
|
||||||
def mock_get_side_effect(key, default=None):
|
def mock_get_side_effect(key, default=None):
|
||||||
if key == "defaults.image":
|
if key == "defaults.driver":
|
||||||
return "goose"
|
return "goose"
|
||||||
elif key == "defaults.volumes":
|
elif key == "defaults.volumes":
|
||||||
return [] # Return empty list for volumes
|
return [] # Return empty list for volumes
|
||||||
@@ -68,15 +71,15 @@ def test_session_create_basic(cli_runner, mock_container_manager):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Session created successfully" in result.stdout
|
assert "Session created successfully" in result.stdout
|
||||||
|
|
||||||
# Verify container_manager was called with the expected image
|
# Verify container_manager was called with the expected driver
|
||||||
mock_container_manager.create_session.assert_called_once()
|
mock_container_manager.create_session.assert_called_once()
|
||||||
assert (
|
assert (
|
||||||
mock_container_manager.create_session.call_args[1]["image_name"] == "goose"
|
mock_container_manager.create_session.call_args[1]["driver_name"] == "goose"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_session_close(cli_runner, mock_container_manager):
|
def test_session_close(cli_runner, mock_container_manager):
|
||||||
"""Test 'cubbi session close' command."""
|
"""Test 'mc session close' command."""
|
||||||
mock_container_manager.close_session.return_value = True
|
mock_container_manager.close_session.return_value = True
|
||||||
|
|
||||||
result = cli_runner.invoke(app, ["session", "close", "test-session-id"])
|
result = cli_runner.invoke(app, ["session", "close", "test-session-id"])
|
||||||
@@ -87,18 +90,20 @@ def test_session_close(cli_runner, mock_container_manager):
|
|||||||
|
|
||||||
|
|
||||||
def test_session_close_all(cli_runner, mock_container_manager):
|
def test_session_close_all(cli_runner, mock_container_manager):
|
||||||
"""Test 'cubbi session close --all' command."""
|
"""Test 'mc session close --all' command."""
|
||||||
# Set up mock sessions
|
# Set up mock sessions
|
||||||
from cubbi.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
|
|
||||||
# timestamp no longer needed since we don't use created_at in Session
|
timestamp = "2023-01-01T00:00:00Z"
|
||||||
mock_sessions = [
|
mock_sessions = [
|
||||||
Session(
|
Session(
|
||||||
id=f"session-{i}",
|
id=f"session-{i}",
|
||||||
name=f"Session {i}",
|
name=f"Session {i}",
|
||||||
image="goose",
|
driver="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={},
|
ports={},
|
||||||
|
project=None,
|
||||||
|
created_at=timestamp,
|
||||||
)
|
)
|
||||||
for i in range(3)
|
for i in range(3)
|
||||||
]
|
]
|
||||||
|
|||||||
78
uv.lock
generated
78
uv.lock
generated
@@ -75,45 +75,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cubbi"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "docker" },
|
|
||||||
{ name = "pydantic" },
|
|
||||||
{ name = "pyyaml" },
|
|
||||||
{ name = "rich" },
|
|
||||||
{ name = "typer" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "mypy" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "pytest" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "docker", specifier = ">=7.0.0" },
|
|
||||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" },
|
|
||||||
{ name = "pydantic", specifier = ">=2.5.0" },
|
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
|
||||||
{ name = "pyyaml", specifier = ">=6.0.1" },
|
|
||||||
{ name = "rich", specifier = ">=13.6.0" },
|
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.9" },
|
|
||||||
{ name = "typer", specifier = ">=0.9.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [{ name = "pytest", specifier = ">=8.3.5" }]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "docker"
|
name = "docker"
|
||||||
version = "7.1.0"
|
version = "7.1.0"
|
||||||
@@ -158,6 +119,45 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcontainer"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "docker" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "typer" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "docker", specifier = ">=7.0.0" },
|
||||||
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.5.0" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0.1" },
|
||||||
|
{ name = "rich", specifier = ">=13.6.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.9" },
|
||||||
|
{ name = "typer", specifier = ">=0.9.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=8.3.5" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdurl"
|
name = "mdurl"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user