Connections own their credentials

Migration 0038 moves every stored secret (OAuth tokens, MCP OAuth
tokens and client registrations, API keys) into the connection's
encrypted envelope, links API-key tools to one connection per distinct
credential, allows several accounts per provider, and adds
credential_mode to sources and tools. OAuth MCP tools keep resolving
each member's own token, as they did before.

docsgpt.connectors.service is now the only reader of OAuth tokens:
get_valid_token_info refreshes under a row lock and persists rotated
refresh tokens, and a revoked grant flags the connection, pauses its
sources and notifies the owner. Loaders build from a connection
(BaseConnectorLoader.from_connection), so scheduled sync covers Drive,
SharePoint and Confluence sources with no browser. S3 and Reddit keys
stay on the connection instead of in remote_data.

New endpoints: POST /api/connections, /setup, /reconnect,
/picker-token, /claim, DELETE /api/connections/<id>, per-action
permissions and MCP refresh-tools. Upload, file listing, sync and
validate-session take a connection_id; session tokens keep working for
this release. The tool executor reads credentials from the resolved
connection (owner or member mode) and pauses on a Connect card when a
connection needs signing in. docsgpt connectors reencrypt rewrites
stored credentials after a key rotation.
This commit is contained in:
arc53-machine committed 2026-09-28 17:03:09 +01:00
1 parent 667d4bbab0
commit 3511004f50
54 files changed
+4007 -754

No files matched your search

+30
View File
@@ -285,6 +285,28 @@ def _add_deploy_commands(commands) -> None:
dev.set_defaults(func=_deploy("dev"), deploy=True)
def _connectors(args: argparse.Namespace) -> int:
"""``docsgpt connectors reencrypt``: move every credential onto the current key."""
if getattr(args, "connectors_action", None) != "reencrypt":
print("usage: docsgpt connectors reencrypt", file=sys.stderr)
return 2
from docsgpt.connectors.service import reencrypt_all
counts = reencrypt_all()
print(
f"docsgpt: re-encrypted {counts['rewritten']} connection(s), "
f"{counts['current']} already current, {counts['failed']} unreadable",
file=sys.stderr,
)
if counts["failed"]:
print(
"docsgpt: unreadable connections were marked 'Reconnect needed'; "
"their owners must reconnect them.",
file=sys.stderr,
)
return 1 if counts["failed"] else 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="docsgpt", description="DocsGPT: private AI for agents, assistants and search.")
parser.add_argument("--version", action="version", version=f"docsgpt {__version__}")
@@ -315,6 +337,14 @@ def build_parser() -> argparse.ArgumentParser:
migrate.add_argument("--no-create", dest="create_db", action="store_false", help="fail instead of creating a missing database")
migrate.set_defaults(func=_migrate)
connectors = commands.add_parser("connectors", help="manage stored connector credentials")
connector_actions = connectors.add_subparsers(dest="connectors_action", metavar="<action>")
connector_actions.add_parser(
"reencrypt",
help="rewrite every stored credential with ENCRYPTION_SECRET_KEY (after a key rotation)",
)
connectors.set_defaults(func=_connectors)
for name, (module, help_text) in SCRIPTS.items():
commands.add_parser(name, help=f"{help_text} (docsgpt.scripts.{module})", add_help=False)