datacharter 0.3.4__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.4 → datacharter-0.4.0}/CHANGELOG.md +24 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/PKG-INFO +1 -1
- {datacharter-0.3.4 → 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.4 → datacharter-0.4.0}/src/datacharter/agent/tools.py +48 -8
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/cli.py +19 -25
- datacharter-0.4.0/src/datacharter/contracts/access.py +31 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/loader.py +1 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/pii.py +26 -1
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/writer.py +25 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/demo.py +3 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/session.py +8 -0
- {datacharter-0.3.4 → 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.4 → datacharter-0.4.0}/src/datacharter/server/app.py +175 -15
- datacharter-0.3.4/src/datacharter/server/static/assets/index-B_FrSWR_.js → datacharter-0.4.0/src/datacharter/server/static/assets/index-D8N7a0N6.js +42 -42
- datacharter-0.3.4/src/datacharter/server/static/assets/index-DaS_YQZd.css → datacharter-0.4.0/src/datacharter/server/static/assets/index-FN6N1fIA.css +1 -1
- {datacharter-0.3.4 → 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.4 → 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.4 → datacharter-0.4.0}/ui/src/App.tsx +40 -8
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/api.ts +26 -0
- {datacharter-0.3.4 → 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/SourceTree.test.tsx +57 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/SourceTree.tsx +82 -6
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/onboarding.test.ts +9 -1
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/onboarding.ts +7 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/styles.css +36 -0
- datacharter-0.3.4/tests/server/test_cold_start.py +0 -85
- {datacharter-0.3.4 → datacharter-0.4.0}/.github/workflows/banned-terms.yml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/.github/workflows/release.yml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/.github/workflows/test.yml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/.gitignore +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/CONTRIBUTING.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/LICENSE +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/README.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/SECURITY.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/THIRD-PARTY-NOTICES.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/TRADEMARKS.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/README.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/demo.gif +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/hero.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/mark.svg +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/screenshot-m4-agent.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/screenshot-m5-complete.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/screenshot-m5-core.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/brand/screenshot-m5-features.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/compose.yaml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/_config.yml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/about.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/agent.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/assets/add-source.gif +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/assets/drag-drop.gif +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/assets/hero.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/assets/screenshot.png +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/assets/snapshot.gif +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/charter-yaml.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/cli.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/faq.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/index.html +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/mcp.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/quickstart.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/security.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/sources.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/docs/workspace.md +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/pyproject.toml +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/scripts/build_ui.sh +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/agent/__init__.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/agent/cache.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/agent/llm.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/agent/loop.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/__init__.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/metrics.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/resolve.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/contracts/secrets.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/__init__.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/aggregate.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/guard.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/provenance.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/pushdown.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/scrub.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/snowflake.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/sources.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/engine/statekey.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/mcp/__init__.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/mcp/server.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/__init__.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/llm_admin.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/security.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/source_admin.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/abap-DLDM7-KI.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/apex-DNDY2TF8.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/azcli-Y6nb8tq_.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/bat-BwHxbl9M.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/bicep-CFznDFnq.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/cameligo-Bf6VGUru.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/clojure-Dnu-v4kV.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/codicon-Brq4_Ui5.ttf +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/coffee-Bd8akH9Z.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/cpp-BbWJElDN.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/csharp-Co3qMtFm.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/csp-D-4FJmMZ.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/css-DdJfP1eB.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/css.worker-CqwWTjeG.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/cssMode-BuAVOLr4.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/cypher-cTPe9QuQ.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/dart-BOtBlQCF.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/dockerfile-BG73LgW2.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/ecl-BEgZUVRK.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/editor.worker-DCKwvLbM.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/elixir-BkW5O-1t.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/flow9-BeJ5waoc.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/freemarker2-DM0thzwF.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/fsharp-PahG7c26.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/go-acbASCJo.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/graphql-BxJiqAUM.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/handlebars-BkMf3Qj4.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/hcl-DtV1sZF8.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/html-KeNy2AFd.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/html.worker-DhJ4VZMp.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/htmlMode-CN5rSB9p.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/ini-Kd9XrMLS.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/java-CXBNlu9o.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/javascript-CZu1iw4q.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/json.worker-BF9eV5mS.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/jsonMode-BtMXpE2Q.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/julia-cl7-CwDS.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/kotlin-s7OhZKlX.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/less-9HpZscsL.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/lexon-OrD6JF1K.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/liquid-mhqehfbE.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/lspLanguageFeatures-D1NXFkgg.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/lua-Cyyb5UIc.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/m3-B8OfTtLu.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/markdown-BFxVWTOG.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/mdx-69yOd9f1.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/mips-CiqrrVzr.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/monaco-CB2SckMT.css +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/monaco-DNze5gXB.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/msdax-DmeGPVcC.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/mysql-C_tMU-Nz.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/objective-c-BDtDVThU.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/pascal-vHIfCaH5.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/pascaligo-DtZ0uQbO.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/perl-Ub6l9XKa.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/pgsql-BlNEE0v7.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/php-BBUBE1dy.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/pla-DSh2-awV.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/postiats-CocnycG-.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/powerquery-tScXyioY.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/powershell-COWaemsV.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/protobuf-Brw8urJB.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/pug-8SOpv6rk.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/python-C5qpjWC4.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/qsharp-Bw9ernYp.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/r-j7ic8hl3.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/razor-fg0l3XC9.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/redis-Bu5POkcn.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/redshift-Bs9aos_-.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/restructuredtext-CqXO7rUv.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/ruby-zBfavPgS.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/rust-BzKRNQWT.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/sb-BBc9UKZt.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/scala-D9hQfWCl.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/scheme-BPhDTwHR.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/scss-CBJaRo0y.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/shell-DiJ1NA_G.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/solidity-Db0IVjzk.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/sophia-CnS9iZB_.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/sparql-CJmd_6j2.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/sql-ClhHkBeG.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/st-CHwy0fLd.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/swift-Bqt4WxQ4.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/systemverilog-Bs9z6M-B.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/tcl-Dm6ycUr_.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/ts.worker-DTZAwq0V.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/tsMode-CaXNdXrQ.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/twig-Csy3S7wG.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/typescript-CCbVPqil.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/typespec-Btyra-wh.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/vb-Db0cS2oM.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/wgsl-BTesnYfV.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/workers-CwEVRSMl.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/xml-CIpcyvha.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/src/datacharter/server/static/assets/yaml-CiikAWeE.js +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/agent/test_agent.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/agent/test_cache.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/agent/test_llm_wire.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/contracts/test_loader.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/contracts/test_metrics.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/contracts/test_pii.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/contracts/test_secrets_store.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_aggregate.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_diff.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_federation.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_guard.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_provenance.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_pushdown.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_scrub.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_session.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_source_lifecycle.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/engine/test_sources.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/integration/seed/mysql.sql +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/integration/seed/postgres.sql +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/integration/test_live_sources.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/mcp/test_mcp_server.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_api.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_llm_admin.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_offline.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_security.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_source_admin.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/server/test_sources_api.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_agent_vidaimock_e2e.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_cli_init.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_drift.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_explain.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_packaging_e2e.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_sample.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_scaffold.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_secrets_cli.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/tests/test_snapshot_recheck.py +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/index.html +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/package-lock.json +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/package.json +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/ChartPanel.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/EmptyState.test.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/EmptyState.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/HelpModal.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/LLMConfig.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/QueryFiles.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/ResultsGrid.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/SourceForm.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/SourcesView.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/components/Tutorial.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/lib/caption.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/lib/chartSpec.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/lib/columns.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/lib/useResize.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/main.tsx +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/monaco.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/test/setup.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/src/vite-env.d.ts +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/tsconfig.json +0 -0
- {datacharter-0.3.4 → datacharter-0.4.0}/ui/vite.config.ts +0 -0
|
@@ -5,6 +5,30 @@ 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
|
+
|
|
8
32
|
## [0.3.4] - 2026-07-24
|
|
9
33
|
|
|
10
34
|
### Added
|
|
@@ -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
|
|
@@ -160,7 +160,8 @@ def _cmd_serve(args: argparse.Namespace) -> int:
|
|
|
160
160
|
ws = _resolve_serve_workspace(args.directory)
|
|
161
161
|
llm = None if args.offline else (_local_llm(args.model) if args.local else None)
|
|
162
162
|
app = create_app(
|
|
163
|
-
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,
|
|
164
165
|
)
|
|
165
166
|
if args.offline:
|
|
166
167
|
_print_attestation(ws, args.host, args.port)
|
|
@@ -203,9 +204,17 @@ def _cmd_secrets(args: argparse.Namespace) -> int:
|
|
|
203
204
|
def _cmd_mcp(args: argparse.Namespace) -> int:
|
|
204
205
|
import asyncio
|
|
205
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
|
+
|
|
206
216
|
from datacharter.agent.tools import ToolBox
|
|
207
217
|
from datacharter.contracts import load_charter
|
|
208
|
-
from datacharter.mcp.server import serve_stdio
|
|
209
218
|
|
|
210
219
|
ws = Path(args.directory).resolve()
|
|
211
220
|
if not (ws / "charter.yaml").exists():
|
|
@@ -254,29 +263,9 @@ def _cmd_diff(args: argparse.Namespace) -> int:
|
|
|
254
263
|
|
|
255
264
|
async def _scan_pii(engine) -> dict[str, list[str]]:
|
|
256
265
|
"""Suggest PII columns per relation: by name, then by sampled values."""
|
|
257
|
-
from datacharter.contracts.pii import
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
idx = {c: i for i, c in enumerate(tables.columns)}
|
|
261
|
-
suggestions: dict[str, list[str]] = {}
|
|
262
|
-
for row in tables.rows:
|
|
263
|
-
db = row[idx["database"]]
|
|
264
|
-
if db in ("system", "temp"):
|
|
265
|
-
continue
|
|
266
|
-
table = row[idx["name"]]
|
|
267
|
-
relation = table if db == "memory" else f"{db}.{table}"
|
|
268
|
-
columns = list(row[idx["column_names"]])
|
|
269
|
-
flagged = set(classify_pii(columns))
|
|
270
|
-
remaining = [c for c in columns if c not in flagged]
|
|
271
|
-
if remaining and all(ch.isalnum() or ch in "._" for ch in relation):
|
|
272
|
-
sample = await engine.query(f"SELECT * FROM {relation} LIMIT 25", timeout_s=30)
|
|
273
|
-
pos = {c: i for i, c in enumerate(sample.columns)}
|
|
274
|
-
for col in remaining:
|
|
275
|
-
if col in pos and detect_value_pii([r[pos[col]] for r in sample.rows]):
|
|
276
|
-
flagged.add(col)
|
|
277
|
-
if flagged:
|
|
278
|
-
suggestions[relation] = [c for c in columns if c in flagged]
|
|
279
|
-
return suggestions
|
|
266
|
+
from datacharter.contracts.pii import detect_pii
|
|
267
|
+
|
|
268
|
+
return await detect_pii(engine)
|
|
280
269
|
|
|
281
270
|
|
|
282
271
|
def _cmd_scan(args: argparse.Namespace) -> int:
|
|
@@ -601,6 +590,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
601
590
|
"mcp", help="Run an MCP server over stdio exposing the governed query tools"
|
|
602
591
|
)
|
|
603
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
|
+
)
|
|
604
598
|
p_mcp.set_defaults(func=_cmd_mcp)
|
|
605
599
|
|
|
606
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
|
|
|
@@ -21,6 +21,9 @@ def write_demo_data(workspace: Path) -> None:
|
|
|
21
21
|
demo.mkdir(exist_ok=True)
|
|
22
22
|
con = sqlite3.connect(str(demo / "store.db"))
|
|
23
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")
|
|
24
27
|
con.execute("CREATE TABLE customers (id INTEGER, email TEXT, tier TEXT)")
|
|
25
28
|
con.executemany(
|
|
26
29
|
"INSERT INTO customers VALUES (?, ?, ?)",
|
|
@@ -467,6 +467,14 @@ class Engine:
|
|
|
467
467
|
if staged is not None:
|
|
468
468
|
conn.execute(f"DROP TABLE IF EXISTS {staged}")
|
|
469
469
|
|
|
470
|
+
def drop_local(self, name: str) -> None:
|
|
471
|
+
"""Drop a persisted snapshot table `local.<name>`."""
|
|
472
|
+
if not re.fullmatch(r"[a-z][a-z0-9_]{0,62}", name):
|
|
473
|
+
raise EngineError("Snapshot name must be lowercase letters, digits, underscores.")
|
|
474
|
+
conn = self._require_conn()
|
|
475
|
+
with self._exec_lock:
|
|
476
|
+
conn.execute(f"DROP TABLE IF EXISTS local.{name}")
|
|
477
|
+
|
|
470
478
|
# -- internals ----------------------------------------------------------
|
|
471
479
|
|
|
472
480
|
def _attach_local(self, state: Path) -> None:
|
|
@@ -58,6 +58,8 @@ class Source(BaseModel):
|
|
|
58
58
|
path: str | None = None
|
|
59
59
|
tables: list[str] = Field(default_factory=list)
|
|
60
60
|
pii: dict[str, list[str]] = Field(default_factory=dict)
|
|
61
|
+
#: Agent-access overrides (on=real, off=masked) at source/table/column level.
|
|
62
|
+
agent_access: dict = Field(default_factory=dict)
|
|
61
63
|
#: Connector-extract cap (Snowflake). Overrides the engine default; ignored
|
|
62
64
|
#: for ATTACH/file sources, which stream and are not row-capped.
|
|
63
65
|
max_rows: int | None = Field(default=None, gt=0)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Which backend powers the chat agent: the OpenAI-wire LLM, or local Claude Code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_CONFIG_FILE = ".datacharter/agent.json"
|
|
9
|
+
_VALID = ("llm", "claude-code")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_backend(workspace: Path) -> str:
|
|
13
|
+
path = workspace / _CONFIG_FILE
|
|
14
|
+
if not path.exists():
|
|
15
|
+
return "llm"
|
|
16
|
+
try:
|
|
17
|
+
backend = json.loads(path.read_text()).get("backend", "llm")
|
|
18
|
+
except Exception:
|
|
19
|
+
return "llm"
|
|
20
|
+
return backend if backend in _VALID else "llm"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_backend(workspace: Path, backend: str) -> None:
|
|
24
|
+
if backend not in _VALID:
|
|
25
|
+
raise ValueError(f"unknown backend: {backend!r}")
|
|
26
|
+
path = workspace / _CONFIG_FILE
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
path.write_text(json.dumps({"backend": backend}))
|