datacharter 0.3.3__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datacharter-0.3.3 → datacharter-0.4.0}/.github/workflows/test.yml +2 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/CHANGELOG.md +33 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/PKG-INFO +1 -1
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/__init__.py +1 -1
- datacharter-0.4.0/src/datacharter/agent/claude_code.py +201 -0
- datacharter-0.4.0/src/datacharter/agent/remote_tools.py +24 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/agent/tools.py +48 -8
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/cli.py +22 -56
- datacharter-0.4.0/src/datacharter/contracts/access.py +31 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/loader.py +1 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/pii.py +26 -1
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/writer.py +25 -0
- datacharter-0.4.0/src/datacharter/demo.py +46 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/session.py +8 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/models.py +2 -0
- datacharter-0.4.0/src/datacharter/server/agent_backend.py +28 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/app.py +198 -17
- datacharter-0.4.0/src/datacharter/server/static/assets/index-D8N7a0N6.js +298 -0
- datacharter-0.3.3/src/datacharter/server/static/assets/index-CX4OWOHb.css → datacharter-0.4.0/src/datacharter/server/static/assets/index-FN6N1fIA.css +1 -1
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/index.html +2 -2
- datacharter-0.4.0/tests/agent/test_claude_code.py +64 -0
- datacharter-0.4.0/tests/agent/test_remote_tools.py +28 -0
- datacharter-0.4.0/tests/agent/test_tools_masking.py +92 -0
- datacharter-0.4.0/tests/contracts/test_access.py +31 -0
- datacharter-0.4.0/tests/contracts/test_detect_pii.py +13 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/contracts/test_writer.py +31 -1
- datacharter-0.4.0/tests/server/test_agent_access_api.py +80 -0
- datacharter-0.4.0/tests/server/test_claude_agent_api.py +84 -0
- datacharter-0.4.0/tests/server/test_cold_start.py +155 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/package-lock.json +1202 -37
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/package.json +9 -2
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/App.tsx +80 -37
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/api.ts +28 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/ChatPanel.tsx +18 -6
- datacharter-0.4.0/ui/src/components/ClaudeCodeConnect.test.tsx +50 -0
- datacharter-0.4.0/ui/src/components/ClaudeCodeConnect.tsx +52 -0
- datacharter-0.4.0/ui/src/components/EmptyState.test.tsx +31 -0
- datacharter-0.4.0/ui/src/components/EmptyState.tsx +53 -0
- datacharter-0.4.0/ui/src/components/SourceTree.test.tsx +57 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/SourceTree.tsx +82 -6
- datacharter-0.4.0/ui/src/onboarding.test.ts +28 -0
- datacharter-0.4.0/ui/src/onboarding.ts +28 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/styles.css +50 -0
- datacharter-0.4.0/ui/src/test/setup.ts +1 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/vite.config.ts +6 -1
- datacharter-0.3.3/src/datacharter/server/static/assets/index-CHFhM_Tn.js +0 -298
- datacharter-0.3.3/tests/server/test_cold_start.py +0 -68
- {datacharter-0.3.3 → datacharter-0.4.0}/.github/workflows/banned-terms.yml +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/.github/workflows/release.yml +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/.gitignore +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/CONTRIBUTING.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/LICENSE +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/README.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/SECURITY.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/THIRD-PARTY-NOTICES.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/TRADEMARKS.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/README.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/demo.gif +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/hero.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/mark.svg +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/screenshot-m4-agent.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/screenshot-m5-complete.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/screenshot-m5-core.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/brand/screenshot-m5-features.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/compose.yaml +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/_config.yml +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/about.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/agent.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/assets/add-source.gif +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/assets/drag-drop.gif +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/assets/hero.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/assets/screenshot.png +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/assets/snapshot.gif +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/charter-yaml.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/cli.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/faq.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/index.html +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/mcp.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/quickstart.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/security.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/sources.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/docs/workspace.md +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/pyproject.toml +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/scripts/build_ui.sh +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/agent/__init__.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/agent/cache.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/agent/llm.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/agent/loop.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/__init__.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/metrics.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/resolve.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/contracts/secrets.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/__init__.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/aggregate.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/guard.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/provenance.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/pushdown.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/scrub.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/snowflake.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/sources.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/engine/statekey.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/mcp/__init__.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/mcp/server.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/__init__.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/llm_admin.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/security.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/source_admin.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/abap-DLDM7-KI.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/apex-DNDY2TF8.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/azcli-Y6nb8tq_.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/bat-BwHxbl9M.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/bicep-CFznDFnq.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/cameligo-Bf6VGUru.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/clojure-Dnu-v4kV.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/codicon-Brq4_Ui5.ttf +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/coffee-Bd8akH9Z.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/cpp-BbWJElDN.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/csharp-Co3qMtFm.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/csp-D-4FJmMZ.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/css-DdJfP1eB.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/css.worker-CqwWTjeG.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/cssMode-BuAVOLr4.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/cypher-cTPe9QuQ.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/dart-BOtBlQCF.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/dockerfile-BG73LgW2.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/ecl-BEgZUVRK.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/editor.worker-DCKwvLbM.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/elixir-BkW5O-1t.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/flow9-BeJ5waoc.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/freemarker2-DM0thzwF.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/fsharp-PahG7c26.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/go-acbASCJo.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/graphql-BxJiqAUM.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/handlebars-BkMf3Qj4.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/hcl-DtV1sZF8.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/html-KeNy2AFd.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/html.worker-DhJ4VZMp.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/htmlMode-CN5rSB9p.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/ini-Kd9XrMLS.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/java-CXBNlu9o.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/javascript-CZu1iw4q.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/json.worker-BF9eV5mS.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/jsonMode-BtMXpE2Q.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/julia-cl7-CwDS.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/kotlin-s7OhZKlX.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/less-9HpZscsL.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/lexon-OrD6JF1K.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/liquid-mhqehfbE.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/lspLanguageFeatures-D1NXFkgg.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/lua-Cyyb5UIc.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/m3-B8OfTtLu.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/markdown-BFxVWTOG.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/mdx-69yOd9f1.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/mips-CiqrrVzr.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/monaco-CB2SckMT.css +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/monaco-DNze5gXB.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/msdax-DmeGPVcC.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/mysql-C_tMU-Nz.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/objective-c-BDtDVThU.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/pascal-vHIfCaH5.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/pascaligo-DtZ0uQbO.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/perl-Ub6l9XKa.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/pgsql-BlNEE0v7.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/php-BBUBE1dy.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/pla-DSh2-awV.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/postiats-CocnycG-.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/powerquery-tScXyioY.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/powershell-COWaemsV.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/protobuf-Brw8urJB.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/pug-8SOpv6rk.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/python-C5qpjWC4.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/qsharp-Bw9ernYp.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/r-j7ic8hl3.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/razor-fg0l3XC9.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/redis-Bu5POkcn.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/redshift-Bs9aos_-.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/restructuredtext-CqXO7rUv.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/ruby-zBfavPgS.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/rust-BzKRNQWT.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/sb-BBc9UKZt.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/scala-D9hQfWCl.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/scheme-BPhDTwHR.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/scss-CBJaRo0y.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/shell-DiJ1NA_G.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/solidity-Db0IVjzk.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/sophia-CnS9iZB_.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/sparql-CJmd_6j2.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/sql-ClhHkBeG.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/st-CHwy0fLd.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/swift-Bqt4WxQ4.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/systemverilog-Bs9z6M-B.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/tcl-Dm6ycUr_.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/ts.worker-DTZAwq0V.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/tsMode-CaXNdXrQ.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/twig-Csy3S7wG.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/typescript-CCbVPqil.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/typespec-Btyra-wh.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/vb-Db0cS2oM.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/wgsl-BTesnYfV.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/workers-CwEVRSMl.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/xml-CIpcyvha.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/src/datacharter/server/static/assets/yaml-CiikAWeE.js +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/agent/test_agent.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/agent/test_cache.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/agent/test_llm_wire.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/contracts/test_loader.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/contracts/test_metrics.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/contracts/test_pii.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/contracts/test_secrets_store.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_aggregate.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_diff.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_federation.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_guard.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_provenance.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_pushdown.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_scrub.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_session.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_source_lifecycle.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/engine/test_sources.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/integration/seed/mysql.sql +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/integration/seed/postgres.sql +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/integration/test_live_sources.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/mcp/test_mcp_server.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_api.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_llm_admin.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_offline.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_security.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_source_admin.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/server/test_sources_api.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_agent_vidaimock_e2e.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_cli_init.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_drift.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_explain.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_packaging_e2e.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_sample.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_scaffold.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_secrets_cli.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/tests/test_snapshot_recheck.py +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/index.html +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/ChartPanel.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/HelpModal.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/LLMConfig.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/QueryFiles.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/ResultsGrid.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/SourceForm.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/SourcesView.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/components/Tutorial.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/lib/caption.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/lib/chartSpec.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/lib/columns.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/lib/useResize.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/main.tsx +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/monaco.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/src/vite-env.d.ts +0 -0
- {datacharter-0.3.3 → datacharter-0.4.0}/ui/tsconfig.json +0 -0
|
@@ -5,6 +5,39 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-07-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Connect Claude Code:** run the chat agent on your local Claude Code subscription (no
|
|
13
|
+
API key) instead of an OpenAI-wire LLM — surfaced beside "Connect an LLM". Data stays
|
|
14
|
+
governed (Claude reaches it only through datacharter's read-only, PII-masked tools via a
|
|
15
|
+
loopback bridge), and the connection is refused, fail-closed, unless the tool sandbox
|
|
16
|
+
verifies (auto-denying any non-governed tools). Requires a Claude Pro/Max subscription +
|
|
17
|
+
Claude Code installed.
|
|
18
|
+
- **Per-field agent-access toggles (left panel):** control per field / table / source
|
|
19
|
+
whether the agent sees real values or masked (`•••`) ones, persisted to the contract
|
|
20
|
+
(`agent_access:`). PII (declared or auto-detected) defaults to masked; everything else to
|
|
21
|
+
real; an explicit toggle wins. Enforced on the agent surface only (agent, MCP, Claude
|
|
22
|
+
Code) — the human SQL editor is never masked. "Agent view" mirrors the toggles.
|
|
23
|
+
- Remove snapshots and uploaded tables directly from the sidebar (a ✕ on `local.*` and
|
|
24
|
+
uploaded rows), via `DELETE /api/snapshot/{name}` and `DELETE /api/uploads/{name}`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The sidebar listed each attached-source table twice — once correctly and once as a fake
|
|
29
|
+
"upload" — because the engine's internal `<source>__<table>` compatibility views were
|
|
30
|
+
included in the catalog listing. They're now hidden.
|
|
31
|
+
|
|
32
|
+
## [0.3.4] - 2026-07-24
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Empty workspaces get an "add your data" first-run launchpad (add a source / drop a
|
|
37
|
+
CSV / load the demo dataset) instead of the demo feature-tour, which now auto-shows
|
|
38
|
+
only when the workspace has sources. New `POST /api/demo` loads the demo `store` as a
|
|
39
|
+
normal, deletable source.
|
|
40
|
+
|
|
8
41
|
## [0.3.3] - 2026-07-24
|
|
9
42
|
|
|
10
43
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datacharter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Charter your data — contract-governed local data exploration, powered by DuckDB
|
|
5
5
|
Project-URL: Homepage, https://github.com/datacharter/datacharter
|
|
6
6
|
Project-URL: Repository, https://github.com/datacharter/datacharter
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Drive the local Claude Code (headless, subscription) as datacharter's agent backend.
|
|
2
|
+
|
|
3
|
+
Governance: every invocation is locked down (strict MCP config + a deny-list settings file +
|
|
4
|
+
`--permission-mode dontAsk`) and gated by a fail-closed tool-surface assertion. Claude reaches
|
|
5
|
+
data only through the governed `datacharter mcp --serve-url` proxy. Never uses `--bare` — that
|
|
6
|
+
disables subscription auth. Design: docs/superpowers/specs/2026-07-24-claude-code-agent-backend-design.md
|
|
7
|
+
""" # noqa: E501
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from collections.abc import AsyncIterator, Iterable, Iterator
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
GOVERNED_TOOLS = [
|
|
21
|
+
"mcp__datacharter__query",
|
|
22
|
+
"mcp__datacharter__list_tables",
|
|
23
|
+
"mcp__datacharter__list_sources",
|
|
24
|
+
"mcp__datacharter__describe_table",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Best-effort removal of built-ins. The connect-time assertion is the real guarantee;
|
|
28
|
+
# extend this from a clean-env `init.tools` list if the assertion reports a stray built-in.
|
|
29
|
+
_DENY = [
|
|
30
|
+
"Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch", "Agent",
|
|
31
|
+
"Workflow", "Skill", "ToolSearch", "TaskCreate", "TaskGet", "TaskList", "TaskUpdate",
|
|
32
|
+
"TaskStop", "TaskOutput", "ScheduleWakeup", "CronCreate", "CronDelete", "CronList",
|
|
33
|
+
"SendMessage", "NotebookEdit", "Monitor", "WaitForMcpServers", "Artifact",
|
|
34
|
+
"AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "ReportFindings",
|
|
35
|
+
"DesignSync", "EnterWorktree", "ExitWorktree", "PushNotification", "RemoteTrigger",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ClaudeGovernanceError(RuntimeError):
|
|
40
|
+
"""Raised when Claude Code exposes tools beyond the governed set — connection refused."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def claude_available() -> bool:
|
|
44
|
+
return shutil.which("claude") is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _dc_bin() -> str:
|
|
48
|
+
# Must be the SAME datacharter that's serving (it needs `mcp --serve-url`); prefer the
|
|
49
|
+
# console script next to the running interpreter over whatever is first on PATH.
|
|
50
|
+
sibling = Path(sys.executable).parent / "datacharter"
|
|
51
|
+
if sibling.exists():
|
|
52
|
+
return str(sibling)
|
|
53
|
+
return shutil.which("datacharter") or "datacharter"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tool_surface_ok(tools: list[str]) -> bool:
|
|
57
|
+
return set(tools).issubset(set(GOVERNED_TOOLS))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_configs(
|
|
61
|
+
serve_url: str, dc_bin: str, tmpdir: Path, deny: list[str] | None = None
|
|
62
|
+
) -> tuple[Path, Path]:
|
|
63
|
+
settings = tmpdir / "settings.json"
|
|
64
|
+
mcp = tmpdir / "mcp.json"
|
|
65
|
+
settings.write_text(
|
|
66
|
+
json.dumps(
|
|
67
|
+
{
|
|
68
|
+
"defaultMode": "dontAsk",
|
|
69
|
+
"permissions": {"allow": GOVERNED_TOOLS, "deny": deny or _DENY},
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
mcp.write_text(
|
|
74
|
+
json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"datacharter": {"command": dc_bin, "args": ["mcp", "--serve-url", serve_url]}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return settings, mcp
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _base_flags(settings: Path, mcp: Path) -> list[str]:
|
|
86
|
+
return [
|
|
87
|
+
"--strict-mcp-config", "--mcp-config", str(mcp),
|
|
88
|
+
"--settings", str(settings), "--permission-mode", "dontAsk",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _env() -> dict:
|
|
93
|
+
env = dict(os.environ)
|
|
94
|
+
env.pop("ANTHROPIC_API_KEY", None) # force subscription auth
|
|
95
|
+
return env
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_stream(lines: Iterable[str]) -> Iterator[dict]:
|
|
99
|
+
"""Map claude `stream-json` NDJSON lines to `{kind, ...}` events.
|
|
100
|
+
|
|
101
|
+
kinds: `session` (init: session_id + tools), `tool_call` (governed tool + SQL),
|
|
102
|
+
`text` (assistant delta), `result` (final text + session_id + is_error)."""
|
|
103
|
+
for raw in lines:
|
|
104
|
+
raw = raw.strip()
|
|
105
|
+
if not raw:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
m = json.loads(raw)
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
continue
|
|
111
|
+
mtype = m.get("type")
|
|
112
|
+
if mtype == "system" and m.get("subtype") == "init":
|
|
113
|
+
yield {
|
|
114
|
+
"kind": "session",
|
|
115
|
+
"session_id": m.get("session_id"),
|
|
116
|
+
"tools": m.get("tools", []),
|
|
117
|
+
}
|
|
118
|
+
elif mtype == "stream_event":
|
|
119
|
+
ev = m.get("event", {}) or {}
|
|
120
|
+
name = str(ev.get("name", ""))
|
|
121
|
+
if ev.get("type") == "tool_use" and name.startswith("mcp__datacharter__"):
|
|
122
|
+
label = ev["name"].removeprefix("mcp__datacharter__")
|
|
123
|
+
sql = (ev.get("input") or {}).get("sql")
|
|
124
|
+
yield {"kind": "tool_call", "tool": f"{label}: {sql}" if sql else label}
|
|
125
|
+
delta = ev.get("delta") or {}
|
|
126
|
+
if delta.get("type") == "text_delta" and delta.get("text"):
|
|
127
|
+
yield {"kind": "text", "text": delta["text"]}
|
|
128
|
+
elif mtype == "result":
|
|
129
|
+
yield {
|
|
130
|
+
"kind": "result",
|
|
131
|
+
"session_id": m.get("session_id"),
|
|
132
|
+
"text": m.get("result", ""),
|
|
133
|
+
"is_error": m.get("is_error", False),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def probe_tools(
|
|
138
|
+
serve_url: str, dc_bin: str | None = None, deny: list[str] | None = None
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
"""Spawn a locked-down claude and return the tool names it actually exposes (init.tools)."""
|
|
141
|
+
dc_bin = dc_bin or _dc_bin()
|
|
142
|
+
with tempfile.TemporaryDirectory(prefix="dc-claude-") as td:
|
|
143
|
+
settings, mcp = build_configs(serve_url, dc_bin, Path(td), deny)
|
|
144
|
+
proc = await asyncio.create_subprocess_exec(
|
|
145
|
+
"claude", "-p", "ok", "--output-format", "stream-json", "--verbose",
|
|
146
|
+
*_base_flags(settings, mcp),
|
|
147
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=_env(),
|
|
148
|
+
)
|
|
149
|
+
out, _ = await proc.communicate()
|
|
150
|
+
for ev in parse_stream(out.decode().splitlines()):
|
|
151
|
+
if ev["kind"] == "session":
|
|
152
|
+
return ev.get("tools", [])
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def assert_tool_surface(serve_url: str, dc_bin: str | None = None) -> list[str]:
|
|
157
|
+
"""Fail-closed: probe Claude's tool surface, auto-deny any non-governed tools it finds,
|
|
158
|
+
and re-probe until only the governed tools remain — returning the effective deny-list.
|
|
159
|
+
Raises `ClaudeGovernanceError` if a non-governed tool cannot be disabled."""
|
|
160
|
+
dc_bin = dc_bin or _dc_bin()
|
|
161
|
+
deny = list(_DENY)
|
|
162
|
+
extras: list[str] = []
|
|
163
|
+
for _ in range(4):
|
|
164
|
+
tools = await probe_tools(serve_url, dc_bin, deny)
|
|
165
|
+
extras = sorted(set(tools) - set(GOVERNED_TOOLS))
|
|
166
|
+
if not extras:
|
|
167
|
+
return deny
|
|
168
|
+
deny += [t for t in extras if t not in deny] # deny only removes; loop converges
|
|
169
|
+
raise ClaudeGovernanceError(
|
|
170
|
+
"Refusing to connect: Claude Code exposes non-governed tools that could not be "
|
|
171
|
+
"disabled: " + ", ".join(extras)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def run_turn(
|
|
176
|
+
question: str,
|
|
177
|
+
serve_url: str,
|
|
178
|
+
session_id: str | None = None,
|
|
179
|
+
dc_bin: str | None = None,
|
|
180
|
+
deny: list[str] | None = None,
|
|
181
|
+
) -> AsyncIterator[dict]:
|
|
182
|
+
"""Run one chat turn; yield parsed stream events. Resumes `session_id` for context.
|
|
183
|
+
`deny` is the effective deny-list from the connect-time assertion."""
|
|
184
|
+
dc_bin = dc_bin or _dc_bin()
|
|
185
|
+
with tempfile.TemporaryDirectory(prefix="dc-claude-") as td:
|
|
186
|
+
settings, mcp = build_configs(serve_url, dc_bin, Path(td), deny)
|
|
187
|
+
args = [
|
|
188
|
+
"claude", "-p", question, "--output-format", "stream-json",
|
|
189
|
+
"--verbose", "--include-partial-messages",
|
|
190
|
+
*_base_flags(settings, mcp),
|
|
191
|
+
]
|
|
192
|
+
if session_id:
|
|
193
|
+
args += ["--resume", session_id]
|
|
194
|
+
proc = await asyncio.create_subprocess_exec(
|
|
195
|
+
*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=_env()
|
|
196
|
+
)
|
|
197
|
+
assert proc.stdout is not None
|
|
198
|
+
async for line in proc.stdout:
|
|
199
|
+
for ev in parse_stream([line.decode()]):
|
|
200
|
+
yield ev
|
|
201
|
+
await proc.wait()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""A ToolBox-shaped facade that forwards tool calls to a running `datacharter serve`.
|
|
2
|
+
|
|
3
|
+
Lets the stdio MCP server (`datacharter mcp --serve-url`) expose the governed tools without
|
|
4
|
+
opening a second engine, which would deadlock on the workspace state-DB lock. Governance is
|
|
5
|
+
the serve process's — `/api/tool` runs the same guarded, PII-masking toolbox as `/api/query`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RemoteToolBox:
|
|
14
|
+
def __init__(
|
|
15
|
+
self, serve_url: str, *, _transport: httpx.AsyncBaseTransport | None = None
|
|
16
|
+
) -> None:
|
|
17
|
+
self._url = serve_url.rstrip("/") + "/api/tool"
|
|
18
|
+
self._transport = _transport
|
|
19
|
+
|
|
20
|
+
async def run(self, name: str, arguments: str) -> str:
|
|
21
|
+
async with httpx.AsyncClient(transport=self._transport, timeout=120) as client:
|
|
22
|
+
resp = await client.post(self._url, json={"name": name, "arguments": arguments or "{}"})
|
|
23
|
+
resp.raise_for_status()
|
|
24
|
+
return resp.json()["result"]
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from datacharter.contracts.access import resolve_masked
|
|
8
9
|
from datacharter.engine.session import Engine
|
|
9
10
|
from datacharter.models import QueryResult, Source
|
|
10
11
|
|
|
@@ -63,13 +64,18 @@ _MAX_TOOL_ROWS = 50
|
|
|
63
64
|
class ToolBox:
|
|
64
65
|
"""Executes tool calls against the engine; masks PII columns in returned data."""
|
|
65
66
|
|
|
66
|
-
def __init__(
|
|
67
|
+
def __init__(
|
|
68
|
+
self, engine: Engine, sources: list[Source], *, auto_pii: set[str] | None = None
|
|
69
|
+
) -> None:
|
|
67
70
|
self._engine = engine
|
|
68
71
|
# Column names flagged PII in any source's contract, matched case-insensitively.
|
|
69
72
|
self._pii: set[str] = set()
|
|
70
73
|
for src in sources:
|
|
71
74
|
for cols in src.pii.values():
|
|
72
75
|
self._pii.update(c.lower() for c in cols)
|
|
76
|
+
self._auto_pii = auto_pii or set()
|
|
77
|
+
# Per-source agent-access overrides (on=real, off=masked).
|
|
78
|
+
self._overrides = {s.name: s.agent_access for s in sources if s.agent_access}
|
|
73
79
|
|
|
74
80
|
async def run(self, name: str, arguments: str) -> str:
|
|
75
81
|
try:
|
|
@@ -111,17 +117,51 @@ class ToolBox:
|
|
|
111
117
|
if not _is_safe_relation(relation):
|
|
112
118
|
return "Error: invalid relation name."
|
|
113
119
|
result = await self._engine.query(f"DESCRIBE {relation}", timeout_s=30)
|
|
114
|
-
return self._render(result)
|
|
120
|
+
return self._render(result, set()) # schema is always visible; values masked only in query
|
|
115
121
|
|
|
116
122
|
async def _query(self, args: dict) -> str:
|
|
117
123
|
sql = str(args.get("sql", ""))
|
|
118
124
|
result = await self._engine.query(sql, row_limit=_MAX_TOOL_ROWS)
|
|
119
|
-
return self._render(result)
|
|
120
|
-
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
return self._render(result, self._mask_indices(result))
|
|
126
|
+
|
|
127
|
+
def _masked(self, source: str, table: str, column: str) -> bool:
|
|
128
|
+
return resolve_masked(
|
|
129
|
+
source, table, column,
|
|
130
|
+
declared_pii=self._pii, auto_pii=self._auto_pii, overrides=self._overrides,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _mask_indices(self, result: QueryResult) -> set[int]:
|
|
134
|
+
"""Which output columns to mask. Prefer per-column lineage; when it's missing
|
|
135
|
+
(e.g. SELECT *), resolve each column against the query's touched relations so
|
|
136
|
+
agent-access overrides still apply; last resort is a name-based PII check."""
|
|
137
|
+
prov = result.provenance or {}
|
|
138
|
+
lineage = prov.get("lineage") or {}
|
|
139
|
+
rels = []
|
|
140
|
+
for r in prov.get("relations") or []:
|
|
141
|
+
parts = str(r).split(".")
|
|
142
|
+
if len(parts) >= 2:
|
|
143
|
+
rels.append((parts[-2], parts[-1])) # (source, table)
|
|
144
|
+
idx = set()
|
|
145
|
+
for i, outcol in enumerate(result.columns):
|
|
146
|
+
srcs = lineage.get(outcol)
|
|
147
|
+
if srcs:
|
|
148
|
+
if any(self._lineage_masked(s) for s in srcs):
|
|
149
|
+
idx.add(i)
|
|
150
|
+
elif rels:
|
|
151
|
+
if any(self._masked(s, t, outcol) for (s, t) in rels):
|
|
152
|
+
idx.add(i)
|
|
153
|
+
elif outcol.lower() in self._pii or outcol.lower() in self._auto_pii:
|
|
154
|
+
idx.add(i)
|
|
155
|
+
return idx
|
|
156
|
+
|
|
157
|
+
def _lineage_masked(self, qualified: str) -> bool:
|
|
158
|
+
parts = qualified.split(".")
|
|
159
|
+
if len(parts) >= 3:
|
|
160
|
+
return self._masked(parts[-3], parts[-2], parts[-1])
|
|
161
|
+
name = parts[-1].lower()
|
|
162
|
+
return name in self._pii or name in self._auto_pii
|
|
163
|
+
|
|
164
|
+
def _render(self, result: QueryResult, mask_idx: set[int]) -> str:
|
|
125
165
|
rows = [
|
|
126
166
|
[MASKED if i in mask_idx else v for i, v in enumerate(row)]
|
|
127
167
|
for row in result.rows
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from datacharter import __version__
|
|
10
|
+
from datacharter.demo import write_demo_data
|
|
10
11
|
|
|
11
12
|
CHARTER_TEMPLATE = """\
|
|
12
13
|
# DataCharter workspace — https://github.com/datacharter/datacharter
|
|
@@ -72,7 +73,7 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
72
73
|
(ws / "queries").mkdir(exist_ok=True)
|
|
73
74
|
_ensure_gitignore(ws)
|
|
74
75
|
if args.demo:
|
|
75
|
-
|
|
76
|
+
write_demo_data(ws)
|
|
76
77
|
print(f"Workspace initialized in {ws}.")
|
|
77
78
|
if args.demo:
|
|
78
79
|
print("Demo data in demo/ — try: datacharter serve")
|
|
@@ -87,35 +88,6 @@ def _ensure_gitignore(ws: Path) -> None:
|
|
|
87
88
|
gi.write_text(existing + joiner + GITIGNORE_BLOCK)
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
def _write_demo_data(ws: Path) -> None:
|
|
91
|
-
import sqlite3
|
|
92
|
-
|
|
93
|
-
demo = ws / "demo"
|
|
94
|
-
demo.mkdir(exist_ok=True)
|
|
95
|
-
con = sqlite3.connect(str(demo / "store.db"))
|
|
96
|
-
try:
|
|
97
|
-
con.execute("CREATE TABLE customers (id INTEGER, email TEXT, tier TEXT)")
|
|
98
|
-
con.executemany(
|
|
99
|
-
"INSERT INTO customers VALUES (?, ?, ?)",
|
|
100
|
-
[
|
|
101
|
-
(1, "ada@example.com", "pro"),
|
|
102
|
-
(2, "grace@example.com", "free"),
|
|
103
|
-
(3, "edsger@example.com", "pro"),
|
|
104
|
-
],
|
|
105
|
-
)
|
|
106
|
-
con.execute("CREATE TABLE orders (customer_id INTEGER, total REAL, placed_on TEXT)")
|
|
107
|
-
con.executemany(
|
|
108
|
-
"INSERT INTO orders VALUES (?, ?, ?)",
|
|
109
|
-
[
|
|
110
|
-
((i % 3) + 1, round((i * 37 % 200) + i * 0.5, 2), f"2026-01-{(i % 28) + 1:02d}")
|
|
111
|
-
for i in range(90)
|
|
112
|
-
],
|
|
113
|
-
)
|
|
114
|
-
con.commit()
|
|
115
|
-
finally:
|
|
116
|
-
con.close()
|
|
117
|
-
|
|
118
|
-
|
|
119
91
|
def _resolve_serve_workspace(directory: str) -> Path:
|
|
120
92
|
"""Workspace for serve; ephemeral demo when no charter.yaml (D3, no surprise writes)."""
|
|
121
93
|
ws = Path(directory).resolve()
|
|
@@ -125,7 +97,7 @@ def _resolve_serve_workspace(directory: str) -> Path:
|
|
|
125
97
|
|
|
126
98
|
demo_ws = Path(tempfile.mkdtemp(prefix="datacharter-demo-"))
|
|
127
99
|
(demo_ws / "charter.yaml").write_text(DEMO_CHARTER)
|
|
128
|
-
|
|
100
|
+
write_demo_data(demo_ws)
|
|
129
101
|
print(f"No charter.yaml in {ws} — serving an ephemeral demo workspace ({demo_ws}).")
|
|
130
102
|
print("Run `datacharter init` here to start a real workspace.")
|
|
131
103
|
return demo_ws
|
|
@@ -188,7 +160,8 @@ def _cmd_serve(args: argparse.Namespace) -> int:
|
|
|
188
160
|
ws = _resolve_serve_workspace(args.directory)
|
|
189
161
|
llm = None if args.offline else (_local_llm(args.model) if args.local else None)
|
|
190
162
|
app = create_app(
|
|
191
|
-
ws, allow_spill=not args.no_spill, llm=llm, host=args.host,
|
|
163
|
+
ws, allow_spill=not args.no_spill, llm=llm, host=args.host, port=args.port,
|
|
164
|
+
offline=args.offline,
|
|
192
165
|
)
|
|
193
166
|
if args.offline:
|
|
194
167
|
_print_attestation(ws, args.host, args.port)
|
|
@@ -231,9 +204,17 @@ def _cmd_secrets(args: argparse.Namespace) -> int:
|
|
|
231
204
|
def _cmd_mcp(args: argparse.Namespace) -> int:
|
|
232
205
|
import asyncio
|
|
233
206
|
|
|
207
|
+
from datacharter.mcp.server import serve_stdio
|
|
208
|
+
|
|
209
|
+
if args.serve_url:
|
|
210
|
+
from datacharter.agent.remote_tools import RemoteToolBox
|
|
211
|
+
|
|
212
|
+
print(f"datacharter MCP proxy → {args.serve_url}", file=sys.stderr)
|
|
213
|
+
asyncio.run(serve_stdio(RemoteToolBox(args.serve_url)))
|
|
214
|
+
return 0
|
|
215
|
+
|
|
234
216
|
from datacharter.agent.tools import ToolBox
|
|
235
217
|
from datacharter.contracts import load_charter
|
|
236
|
-
from datacharter.mcp.server import serve_stdio
|
|
237
218
|
|
|
238
219
|
ws = Path(args.directory).resolve()
|
|
239
220
|
if not (ws / "charter.yaml").exists():
|
|
@@ -282,29 +263,9 @@ def _cmd_diff(args: argparse.Namespace) -> int:
|
|
|
282
263
|
|
|
283
264
|
async def _scan_pii(engine) -> dict[str, list[str]]:
|
|
284
265
|
"""Suggest PII columns per relation: by name, then by sampled values."""
|
|
285
|
-
from datacharter.contracts.pii import
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
idx = {c: i for i, c in enumerate(tables.columns)}
|
|
289
|
-
suggestions: dict[str, list[str]] = {}
|
|
290
|
-
for row in tables.rows:
|
|
291
|
-
db = row[idx["database"]]
|
|
292
|
-
if db in ("system", "temp"):
|
|
293
|
-
continue
|
|
294
|
-
table = row[idx["name"]]
|
|
295
|
-
relation = table if db == "memory" else f"{db}.{table}"
|
|
296
|
-
columns = list(row[idx["column_names"]])
|
|
297
|
-
flagged = set(classify_pii(columns))
|
|
298
|
-
remaining = [c for c in columns if c not in flagged]
|
|
299
|
-
if remaining and all(ch.isalnum() or ch in "._" for ch in relation):
|
|
300
|
-
sample = await engine.query(f"SELECT * FROM {relation} LIMIT 25", timeout_s=30)
|
|
301
|
-
pos = {c: i for i, c in enumerate(sample.columns)}
|
|
302
|
-
for col in remaining:
|
|
303
|
-
if col in pos and detect_value_pii([r[pos[col]] for r in sample.rows]):
|
|
304
|
-
flagged.add(col)
|
|
305
|
-
if flagged:
|
|
306
|
-
suggestions[relation] = [c for c in columns if c in flagged]
|
|
307
|
-
return suggestions
|
|
266
|
+
from datacharter.contracts.pii import detect_pii
|
|
267
|
+
|
|
268
|
+
return await detect_pii(engine)
|
|
308
269
|
|
|
309
270
|
|
|
310
271
|
def _cmd_scan(args: argparse.Namespace) -> int:
|
|
@@ -629,6 +590,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
629
590
|
"mcp", help="Run an MCP server over stdio exposing the governed query tools"
|
|
630
591
|
)
|
|
631
592
|
p_mcp.add_argument("directory", nargs="?", default=".")
|
|
593
|
+
p_mcp.add_argument(
|
|
594
|
+
"--serve-url",
|
|
595
|
+
default=None,
|
|
596
|
+
help="Proxy tools to a running `datacharter serve` instead of opening a local engine",
|
|
597
|
+
)
|
|
632
598
|
p_mcp.set_defaults(func=_cmd_mcp)
|
|
633
599
|
|
|
634
600
|
p_diff = sub.add_parser("diff", help="Diff two relations (rows only in each + common count)")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Resolve whether an agent-facing column is masked (access OFF) or real (ON).
|
|
2
|
+
|
|
3
|
+
Precedence: field override -> table override -> source override -> default (masked iff the
|
|
4
|
+
column is PII: declared or auto-detected). In the contract, `on` = real, `off` = masked.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_masked(
|
|
11
|
+
source: str,
|
|
12
|
+
table: str,
|
|
13
|
+
column: str,
|
|
14
|
+
*,
|
|
15
|
+
declared_pii: set[str],
|
|
16
|
+
auto_pii: set[str],
|
|
17
|
+
overrides: dict,
|
|
18
|
+
) -> bool:
|
|
19
|
+
"""True if the agent should see this column masked (`•••`), False for real values."""
|
|
20
|
+
src_ov = overrides.get(source) or {}
|
|
21
|
+
columns = src_ov.get("columns") or {}
|
|
22
|
+
key = f"{table}.{column}"
|
|
23
|
+
if key in columns:
|
|
24
|
+
return not columns[key] # on -> not masked
|
|
25
|
+
tables = src_ov.get("tables") or {}
|
|
26
|
+
if table in tables:
|
|
27
|
+
return not tables[table]
|
|
28
|
+
if "source" in src_ov:
|
|
29
|
+
return not src_ov["source"]
|
|
30
|
+
col = column.lower()
|
|
31
|
+
return col in declared_pii or col in auto_pii # default: PII masked, else real
|
|
@@ -121,6 +121,7 @@ def _build_source(name: str, body: Any, resolver: SecretResolver, warnings: list
|
|
|
121
121
|
path=path_value,
|
|
122
122
|
tables=list(body.get("tables") or []),
|
|
123
123
|
pii={k: list(v) for k, v in (body.get("pii") or {}).items()},
|
|
124
|
+
agent_access=dict(body.get("agent_access") or {}),
|
|
124
125
|
max_rows=max_rows,
|
|
125
126
|
)
|
|
126
127
|
except ValidationError as exc:
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
|
|
14
|
-
__all__ = ["classify_pii", "detect_value_pii", "PII_TOKENS"]
|
|
14
|
+
__all__ = ["classify_pii", "detect_value_pii", "detect_pii", "PII_TOKENS"]
|
|
15
15
|
|
|
16
16
|
# High-precision value patterns only — distinctive enough that matching most of a
|
|
17
17
|
# column's sampled values is strong evidence, without the false positives a loose
|
|
@@ -79,3 +79,28 @@ def detect_value_pii(values: list, *, threshold: float = 0.6) -> str | None:
|
|
|
79
79
|
if sum(1 for s in samples if pattern.match(s)) / len(samples) >= threshold:
|
|
80
80
|
return kind
|
|
81
81
|
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def detect_pii(engine) -> dict[str, list[str]]:
|
|
85
|
+
"""Suggest PII columns per relation: by name, then by sampled values."""
|
|
86
|
+
tables = await engine.query("SHOW ALL TABLES", timeout_s=30)
|
|
87
|
+
idx = {c: i for i, c in enumerate(tables.columns)}
|
|
88
|
+
suggestions: dict[str, list[str]] = {}
|
|
89
|
+
for row in tables.rows:
|
|
90
|
+
db = row[idx["database"]]
|
|
91
|
+
if db in ("system", "temp"):
|
|
92
|
+
continue
|
|
93
|
+
table = row[idx["name"]]
|
|
94
|
+
relation = table if db == "memory" else f"{db}.{table}"
|
|
95
|
+
columns = list(row[idx["column_names"]])
|
|
96
|
+
flagged = set(classify_pii(columns))
|
|
97
|
+
remaining = [c for c in columns if c not in flagged]
|
|
98
|
+
if remaining and all(ch.isalnum() or ch in "._" for ch in relation):
|
|
99
|
+
sample = await engine.query(f"SELECT * FROM {relation} LIMIT 25", timeout_s=30)
|
|
100
|
+
pos = {c: i for i, c in enumerate(sample.columns)}
|
|
101
|
+
for col in remaining:
|
|
102
|
+
if col in pos and detect_value_pii([r[pos[col]] for r in sample.rows]):
|
|
103
|
+
flagged.add(col)
|
|
104
|
+
if flagged:
|
|
105
|
+
suggestions[relation] = [c for c in columns if c in flagged]
|
|
106
|
+
return suggestions
|
|
@@ -64,6 +64,31 @@ def remove_source(workspace: Path, name: str) -> None:
|
|
|
64
64
|
_write(path, y, data)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def set_agent_access(
|
|
68
|
+
workspace: Path, source: str, table: str | None, column: str | None, value: bool
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Persist one agent-access override (on=real, off=masked) at field/table/source level.
|
|
71
|
+
|
|
72
|
+
Touches only the source's `agent_access` block; everything else is preserved verbatim."""
|
|
73
|
+
path = workspace / CHARTER_FILE
|
|
74
|
+
y, data = _load(path)
|
|
75
|
+
sources = data.get("sources") or {}
|
|
76
|
+
if source not in sources:
|
|
77
|
+
raise ContractWriteError(f"Source '{source}' is not in the charter.")
|
|
78
|
+
entry = sources[source]
|
|
79
|
+
aa = entry.get("agent_access")
|
|
80
|
+
if aa is None:
|
|
81
|
+
aa = {}
|
|
82
|
+
entry["agent_access"] = aa
|
|
83
|
+
if column is not None and table is not None:
|
|
84
|
+
aa.setdefault("columns", {})[f"{table}.{column}"] = value
|
|
85
|
+
elif table is not None:
|
|
86
|
+
aa.setdefault("tables", {})[table] = value
|
|
87
|
+
else:
|
|
88
|
+
aa["source"] = value
|
|
89
|
+
_write(path, y, data)
|
|
90
|
+
|
|
91
|
+
|
|
67
92
|
def set_pii(workspace: Path, source: str, table: str, columns: list[str]) -> None:
|
|
68
93
|
"""Merge PII column names into one source's pii map (round-trip).
|
|
69
94
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The demo dataset: a small sqlite `store` (customers + orders) for onboarding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Descriptor for registering the demo as a real, deletable source (see POST /api/demo).
|
|
9
|
+
DEMO_SOURCE = {
|
|
10
|
+
"name": "store",
|
|
11
|
+
"type": "sqlite",
|
|
12
|
+
"path": "demo/store.db",
|
|
13
|
+
"tables": ["customers", "orders"],
|
|
14
|
+
"pii": {"customers": ["email"]},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def write_demo_data(workspace: Path) -> None:
|
|
19
|
+
"""Write the demo `store.db` (customers + orders) under `<workspace>/demo/`."""
|
|
20
|
+
demo = workspace / "demo"
|
|
21
|
+
demo.mkdir(exist_ok=True)
|
|
22
|
+
con = sqlite3.connect(str(demo / "store.db"))
|
|
23
|
+
try:
|
|
24
|
+
# Idempotent: a prior demo may have been loaded then deleted, leaving store.db.
|
|
25
|
+
con.execute("DROP TABLE IF EXISTS customers")
|
|
26
|
+
con.execute("DROP TABLE IF EXISTS orders")
|
|
27
|
+
con.execute("CREATE TABLE customers (id INTEGER, email TEXT, tier TEXT)")
|
|
28
|
+
con.executemany(
|
|
29
|
+
"INSERT INTO customers VALUES (?, ?, ?)",
|
|
30
|
+
[
|
|
31
|
+
(1, "ada@example.com", "pro"),
|
|
32
|
+
(2, "grace@example.com", "free"),
|
|
33
|
+
(3, "edsger@example.com", "pro"),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
con.execute("CREATE TABLE orders (customer_id INTEGER, total REAL, placed_on TEXT)")
|
|
37
|
+
con.executemany(
|
|
38
|
+
"INSERT INTO orders VALUES (?, ?, ?)",
|
|
39
|
+
[
|
|
40
|
+
((i % 3) + 1, round((i * 37 % 200) + i * 0.5, 2), f"2026-01-{(i % 28) + 1:02d}")
|
|
41
|
+
for i in range(90)
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
con.commit()
|
|
45
|
+
finally:
|
|
46
|
+
con.close()
|