stigmem-plugin-multi-tenant 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stigmem_plugin_multi_tenant-0.1.0/.gitignore +70 -0
- stigmem_plugin_multi_tenant-0.1.0/PKG-INFO +102 -0
- stigmem_plugin_multi_tenant-0.1.0/README.md +79 -0
- stigmem_plugin_multi_tenant-0.1.0/STATUS.md +19 -0
- stigmem_plugin_multi_tenant-0.1.0/concept.md +14 -0
- stigmem_plugin_multi_tenant-0.1.0/pyproject.toml +53 -0
- stigmem_plugin_multi_tenant-0.1.0/security.md +28 -0
- stigmem_plugin_multi_tenant-0.1.0/spec.md +122 -0
- stigmem_plugin_multi_tenant-0.1.0/src/stigmem_plugin_multi_tenant/__init__.py +12 -0
- stigmem_plugin_multi_tenant-0.1.0/src/stigmem_plugin_multi_tenant/config.py +28 -0
- stigmem_plugin_multi_tenant-0.1.0/src/stigmem_plugin_multi_tenant/handlers.py +127 -0
- stigmem_plugin_multi_tenant-0.1.0/src/stigmem_plugin_multi_tenant/manifest.py +48 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.uv/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
coverage.xml
|
|
18
|
+
*.cover
|
|
19
|
+
coverage/
|
|
20
|
+
|
|
21
|
+
# Node / pnpm
|
|
22
|
+
node_modules/
|
|
23
|
+
.next/
|
|
24
|
+
.turbo/
|
|
25
|
+
.jscpd/
|
|
26
|
+
.pnpm-store/
|
|
27
|
+
dist/
|
|
28
|
+
*.tsbuildinfo
|
|
29
|
+
apps/dashboard/coverage/
|
|
30
|
+
adapters/mcp/coverage/
|
|
31
|
+
sdks/stigmem-ts/coverage/
|
|
32
|
+
|
|
33
|
+
# Environment
|
|
34
|
+
.env
|
|
35
|
+
.env.local
|
|
36
|
+
.env.*.local
|
|
37
|
+
|
|
38
|
+
# Release signing keys — public keys are attached to GitHub Releases only;
|
|
39
|
+
# private keys must NEVER be committed (use offline storage).
|
|
40
|
+
stigmem-release-signing-key.asc
|
|
41
|
+
stigmem-release-signing-key*.asc
|
|
42
|
+
*private*signing*key*.asc
|
|
43
|
+
*secret*signing*key*.asc
|
|
44
|
+
|
|
45
|
+
# IDE
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Local Codex project instructions
|
|
56
|
+
AGENTS.md
|
|
57
|
+
|
|
58
|
+
# Docker
|
|
59
|
+
*.log
|
|
60
|
+
|
|
61
|
+
# Stigmem runtime (DB and logs are runtime state, not source)
|
|
62
|
+
data/*.db
|
|
63
|
+
data/*.db-shm
|
|
64
|
+
data/*.db-wal
|
|
65
|
+
stigmem.db
|
|
66
|
+
stigmem.db-shm
|
|
67
|
+
stigmem.db-wal
|
|
68
|
+
logs/
|
|
69
|
+
# Eval results (CI-generated artifacts)
|
|
70
|
+
eval/results/
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stigmem-plugin-multi-tenant
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Experimental multi-tenant isolation plugin for Stigmem.
|
|
5
|
+
Project-URL: Homepage, https://github.com/eidetic-labs/stigmem
|
|
6
|
+
Project-URL: Documentation, https://github.com/eidetic-labs/stigmem/tree/main/features/multi-tenant
|
|
7
|
+
Project-URL: Repository, https://github.com/eidetic-labs/stigmem
|
|
8
|
+
Project-URL: Issues, https://github.com/eidetic-labs/stigmem/issues
|
|
9
|
+
Author-email: Eidetic Labs <oss@eidetic-labs.ai>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: authorization,multi-tenant,plugins,stigmem,tenant-isolation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pydantic<3,>=2
|
|
21
|
+
Requires-Dist: stigmem-node<1.0.0,>=0.9.0a8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Stigmem Multi-Tenant Plugin
|
|
25
|
+
|
|
26
|
+
Experimental multi-tenant scoping plugin for Stigmem.
|
|
27
|
+
|
|
28
|
+
This package provides the `stigmem-plugin-multi-tenant` source package for
|
|
29
|
+
alpha validation. It registers through the `stigmem.plugins` entry point group
|
|
30
|
+
and is loaded by `stigmem-node` only when explicitly installed and configured by
|
|
31
|
+
an operator.
|
|
32
|
+
|
|
33
|
+
## Status
|
|
34
|
+
|
|
35
|
+
Multi-tenant scoping remains experimental. Installing this package does not add
|
|
36
|
+
shared-node readiness to the supported default surface. Default installs
|
|
37
|
+
collapse callers into the `default` tenant unless the plugin is registered and
|
|
38
|
+
`STIGMEM_MULTI_TENANT_ENABLED=true`.
|
|
39
|
+
|
|
40
|
+
The package metadata is publication-shaped for the plugin readiness track, but
|
|
41
|
+
registry publication remains on hold until dry-run evidence and maintainer
|
|
42
|
+
clearance are recorded. See the feature record under `features/multi-tenant/`
|
|
43
|
+
for the current status, evidence, and security notes.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install --pre stigmem-node==0.9.0a8 stigmem-plugin-multi-tenant==0.1.0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Enable
|
|
52
|
+
|
|
53
|
+
Set the plugin gate environment variable to opt in:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export STIGMEM_MULTI_TENANT_ENABLED=1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The default install is inert; multi-tenant hook behavior only activates when
|
|
60
|
+
the package is installed, discovered through the `stigmem.plugins` entry point,
|
|
61
|
+
and the operator enables the gate. Until then callers continue to collapse into
|
|
62
|
+
the core `default` tenant boundary.
|
|
63
|
+
|
|
64
|
+
## Disable
|
|
65
|
+
|
|
66
|
+
Unset the plugin gate environment variable, or set it to any value other than
|
|
67
|
+
`1`, `true`, `yes`, or `on`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
unset STIGMEM_MULTI_TENANT_ENABLED
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The plugin returns to inert state at the next process start. No data migration
|
|
74
|
+
is required; core tenant, scope, and audit isolation continues to hold.
|
|
75
|
+
|
|
76
|
+
## Test
|
|
77
|
+
|
|
78
|
+
From a Stigmem repository checkout with development dependencies installed:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run pytest node/tests/plugins/test_multi_tenant_plugin_scaffold.py \
|
|
82
|
+
node/tests/plugins/test_multi_tenant_plugin_validation.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The package itself ships no separate test tree; upstream plugin validation
|
|
86
|
+
lives in `node/tests/plugins/`.
|
|
87
|
+
|
|
88
|
+
## Uninstall
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip uninstall stigmem-plugin-multi-tenant
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Removing the package is sufficient. The gate environment variable becomes moot
|
|
95
|
+
once the entry point is no longer discoverable.
|
|
96
|
+
|
|
97
|
+
## Project Links
|
|
98
|
+
|
|
99
|
+
- Repository: <https://github.com/eidetic-labs/stigmem>
|
|
100
|
+
- Feature record: <https://github.com/eidetic-labs/stigmem/tree/main/features/multi-tenant>
|
|
101
|
+
- Plugin source: <https://github.com/eidetic-labs/stigmem/tree/main/experimental/multi-tenant>
|
|
102
|
+
- Issue tracker: <https://github.com/eidetic-labs/stigmem/issues>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Stigmem Multi-Tenant Plugin
|
|
2
|
+
|
|
3
|
+
Experimental multi-tenant scoping plugin for Stigmem.
|
|
4
|
+
|
|
5
|
+
This package provides the `stigmem-plugin-multi-tenant` source package for
|
|
6
|
+
alpha validation. It registers through the `stigmem.plugins` entry point group
|
|
7
|
+
and is loaded by `stigmem-node` only when explicitly installed and configured by
|
|
8
|
+
an operator.
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
Multi-tenant scoping remains experimental. Installing this package does not add
|
|
13
|
+
shared-node readiness to the supported default surface. Default installs
|
|
14
|
+
collapse callers into the `default` tenant unless the plugin is registered and
|
|
15
|
+
`STIGMEM_MULTI_TENANT_ENABLED=true`.
|
|
16
|
+
|
|
17
|
+
The package metadata is publication-shaped for the plugin readiness track, but
|
|
18
|
+
registry publication remains on hold until dry-run evidence and maintainer
|
|
19
|
+
clearance are recorded. See the feature record under `features/multi-tenant/`
|
|
20
|
+
for the current status, evidence, and security notes.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install --pre stigmem-node==0.9.0a8 stigmem-plugin-multi-tenant==0.1.0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Enable
|
|
29
|
+
|
|
30
|
+
Set the plugin gate environment variable to opt in:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export STIGMEM_MULTI_TENANT_ENABLED=1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The default install is inert; multi-tenant hook behavior only activates when
|
|
37
|
+
the package is installed, discovered through the `stigmem.plugins` entry point,
|
|
38
|
+
and the operator enables the gate. Until then callers continue to collapse into
|
|
39
|
+
the core `default` tenant boundary.
|
|
40
|
+
|
|
41
|
+
## Disable
|
|
42
|
+
|
|
43
|
+
Unset the plugin gate environment variable, or set it to any value other than
|
|
44
|
+
`1`, `true`, `yes`, or `on`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
unset STIGMEM_MULTI_TENANT_ENABLED
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The plugin returns to inert state at the next process start. No data migration
|
|
51
|
+
is required; core tenant, scope, and audit isolation continues to hold.
|
|
52
|
+
|
|
53
|
+
## Test
|
|
54
|
+
|
|
55
|
+
From a Stigmem repository checkout with development dependencies installed:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run pytest node/tests/plugins/test_multi_tenant_plugin_scaffold.py \
|
|
59
|
+
node/tests/plugins/test_multi_tenant_plugin_validation.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The package itself ships no separate test tree; upstream plugin validation
|
|
63
|
+
lives in `node/tests/plugins/`.
|
|
64
|
+
|
|
65
|
+
## Uninstall
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip uninstall stigmem-plugin-multi-tenant
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Removing the package is sufficient. The gate environment variable becomes moot
|
|
72
|
+
once the entry point is no longer discoverable.
|
|
73
|
+
|
|
74
|
+
## Project Links
|
|
75
|
+
|
|
76
|
+
- Repository: <https://github.com/eidetic-labs/stigmem>
|
|
77
|
+
- Feature record: <https://github.com/eidetic-labs/stigmem/tree/main/features/multi-tenant>
|
|
78
|
+
- Plugin source: <https://github.com/eidetic-labs/stigmem/tree/main/experimental/multi-tenant>
|
|
79
|
+
- Issue tracker: <https://github.com/eidetic-labs/stigmem/issues>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Multi-Tenant Scoping Status
|
|
2
|
+
|
|
3
|
+
This file is a compatibility pointer for existing
|
|
4
|
+
`experimental/multi-tenant/` links.
|
|
5
|
+
|
|
6
|
+
The canonical ADR-020 status record now lives at
|
|
7
|
+
[`features/multi-tenant/status.md`](../../features/multi-tenant/status.md).
|
|
8
|
+
|
|
9
|
+
Current summary:
|
|
10
|
+
|
|
11
|
+
- Status: `active`
|
|
12
|
+
- Stability: `experimental`
|
|
13
|
+
- Default surface: `opt-in`
|
|
14
|
+
- Implementation path: `experimental/multi-tenant/`
|
|
15
|
+
- Package: `stigmem-plugin-multi-tenant`
|
|
16
|
+
- Canonical spec: none currently assigned
|
|
17
|
+
|
|
18
|
+
The implementation package remains here during transition. Product status,
|
|
19
|
+
gates, history, and release-facing facts belong in the feature record.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Multi-Tenant Scoping Concept
|
|
2
|
+
|
|
3
|
+
This file is a compatibility pointer for existing
|
|
4
|
+
`experimental/multi-tenant/concept.md` links.
|
|
5
|
+
|
|
6
|
+
The canonical ADR-020 feature record now lives at
|
|
7
|
+
[`features/multi-tenant/`](../../features/multi-tenant/).
|
|
8
|
+
|
|
9
|
+
- Feature overview: [`features/multi-tenant/README.md`](../../features/multi-tenant/README.md)
|
|
10
|
+
- Behavior spec: [`features/multi-tenant/spec.md`](../../features/multi-tenant/spec.md)
|
|
11
|
+
- Evidence and coverage gaps: [`features/multi-tenant/evidence.md`](../../features/multi-tenant/evidence.md)
|
|
12
|
+
- Security analysis: [`features/multi-tenant/security.md`](../../features/multi-tenant/security.md)
|
|
13
|
+
|
|
14
|
+
Product concept detail belongs in the feature record.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stigmem-plugin-multi-tenant"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Experimental multi-tenant isolation plugin for Stigmem."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Eidetic Labs", email = "oss@eidetic-labs.ai" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["stigmem", "plugins", "multi-tenant", "tenant-isolation", "authorization"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pydantic>=2,<3",
|
|
23
|
+
"stigmem-node>=0.9.0a8,<1.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.entry-points."stigmem.plugins"]
|
|
27
|
+
multi-tenant = "stigmem_plugin_multi_tenant:plugin_manifest"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/eidetic-labs/stigmem"
|
|
31
|
+
Documentation = "https://github.com/eidetic-labs/stigmem/tree/main/features/multi-tenant"
|
|
32
|
+
Repository = "https://github.com/eidetic-labs/stigmem"
|
|
33
|
+
Issues = "https://github.com/eidetic-labs/stigmem/issues"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/stigmem_plugin_multi_tenant"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
43
|
+
"src" = ""
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = [
|
|
47
|
+
"README.md",
|
|
48
|
+
"STATUS.md",
|
|
49
|
+
"security.md",
|
|
50
|
+
"spec.md",
|
|
51
|
+
"concept.md",
|
|
52
|
+
"src/stigmem_plugin_multi_tenant/**/*.py",
|
|
53
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
feature: multi-tenant
|
|
3
|
+
spec_id: Spec-X0-Multi-Tenant-Scoping
|
|
4
|
+
status: Experimental
|
|
5
|
+
applies_to: stigmem v0.9.0a8
|
|
6
|
+
last_updated: 2026-05-24
|
|
7
|
+
owned_risks: []
|
|
8
|
+
contributed_risks:
|
|
9
|
+
- R-01
|
|
10
|
+
- R-02
|
|
11
|
+
- R-21
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Multi-Tenant Scoping Security
|
|
15
|
+
|
|
16
|
+
This file is a compatibility pointer for existing
|
|
17
|
+
`experimental/multi-tenant/` security links.
|
|
18
|
+
|
|
19
|
+
The canonical ADR-020 security record now lives at
|
|
20
|
+
[`features/multi-tenant/security.md`](../../features/multi-tenant/security.md).
|
|
21
|
+
|
|
22
|
+
Current risk summary:
|
|
23
|
+
|
|
24
|
+
- Owned risks: none.
|
|
25
|
+
- Contributed risks: R-01, R-02, and R-21.
|
|
26
|
+
|
|
27
|
+
Product security analysis, operator scenarios, conformance pointers, residual
|
|
28
|
+
risk, and advisory history belong in the feature record.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
spec_id: Spec-X4-Multi-Tenant-Scoping
|
|
3
|
+
version: 0.1.0-alpha.0
|
|
4
|
+
status: Experimental
|
|
5
|
+
applies_to: future experimental plugin line
|
|
6
|
+
last_updated: 2026-05-24
|
|
7
|
+
supersedes: features/multi-tenant/spec.md package projection
|
|
8
|
+
depends_on:
|
|
9
|
+
- Spec-01-Fact-Model >= 0.1.0-alpha.0
|
|
10
|
+
- Spec-02-Scopes-and-ACL >= 0.1.0-alpha.0
|
|
11
|
+
- Spec-03-HTTP-API >= 0.1.0-alpha.0
|
|
12
|
+
- Spec-06-Capability-Tokens >= 0.1.0-alpha.0
|
|
13
|
+
- Spec-07-Recall-Pipeline >= 0.1.0-alpha.0
|
|
14
|
+
- Spec-09-Audit-Log >= 0.1.0-alpha.0
|
|
15
|
+
title: Multi-Tenant Scoping
|
|
16
|
+
sidebar_label: Multi-Tenant Scoping
|
|
17
|
+
audience: Spec
|
|
18
|
+
description: "Package projection for multi-tenant scoping semantics."
|
|
19
|
+
stability: experimental
|
|
20
|
+
since: 0.9.0a8
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Multi-Tenant Scoping Spec
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
Multi-tenant scoping defines how a single Stigmem node can isolate facts,
|
|
28
|
+
gardens, audit records, API keys, recall, and plugin hooks by `tenant_id` when
|
|
29
|
+
the opt-in `stigmem-plugin-multi-tenant` package is registered and explicitly
|
|
30
|
+
enabled.
|
|
31
|
+
|
|
32
|
+
This feature has no Spec-X assignment. If it is reintroduced into the supported
|
|
33
|
+
surface as protocol behavior, it must receive a numbered Spec-X per ADR-010 and
|
|
34
|
+
pass ADR-008 gates.
|
|
35
|
+
|
|
36
|
+
## Default Behavior
|
|
37
|
+
|
|
38
|
+
Default installs resolve every caller into the single `default` tenant. This is
|
|
39
|
+
true even if the stored API key carries a non-default tenant ID.
|
|
40
|
+
|
|
41
|
+
Default behavior exists so the core node can keep storage compatibility columns
|
|
42
|
+
without making multi-tenancy part of the default supported surface.
|
|
43
|
+
Single-tenant deployments do not need the plugin.
|
|
44
|
+
|
|
45
|
+
## Plugin-Enabled Behavior
|
|
46
|
+
|
|
47
|
+
When the plugin is registered and `STIGMEM_MULTI_TENANT_ENABLED=true`, the
|
|
48
|
+
`tenant_resolve` hook preserves the API key's tenant ID. The resolved tenant
|
|
49
|
+
then flows through tenant-scoped HTTP, storage, recall, audit, observability,
|
|
50
|
+
subscription, and plugin-hook paths that accept request tenant context.
|
|
51
|
+
|
|
52
|
+
Existing node-level federation replication remains constrained to the `default`
|
|
53
|
+
tenant in v0.9.0a8. Non-default tenant federation needs a dedicated design,
|
|
54
|
+
conformance vectors, and risk disposition before it can be promoted.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
Bearer token -> Identity(entity_uri, permissions, tenant_id)
|
|
58
|
+
|
|
|
59
|
+
v
|
|
60
|
+
TenantContext(tenant_id)
|
|
61
|
+
|
|
|
62
|
+
v
|
|
63
|
+
storage and hook paths filter by tenant_id
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Isolation Guarantees
|
|
67
|
+
|
|
68
|
+
| Resource | Isolation mechanism |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Facts | `tenant_id` column on `facts`; reads and writes filter by caller tenant. |
|
|
71
|
+
| Gardens | Garden slug lookup is scoped by tenant; the same slug can exist in multiple tenants. |
|
|
72
|
+
| Garden ACL | Membership checks resolve garden identity within the caller tenant. |
|
|
73
|
+
| Audit log | Audit rows carry `tenant_id`; audit queries are tenant-scoped. |
|
|
74
|
+
| API keys | Each key belongs to exactly one tenant. |
|
|
75
|
+
| Federation | Existing node-level pull exports only `default` tenant facts; tenant-aware non-default federation is deferred. |
|
|
76
|
+
| Plugin hooks | Hook payloads receive tenant context through the plugin framework. |
|
|
77
|
+
|
|
78
|
+
## Non-Isolated Node Surfaces
|
|
79
|
+
|
|
80
|
+
The following surfaces are node-level, not tenant-level:
|
|
81
|
+
|
|
82
|
+
- environment settings;
|
|
83
|
+
- health and well-known endpoints;
|
|
84
|
+
- node identity and signing keys;
|
|
85
|
+
- process resources such as CPU, memory, disk, and network capacity.
|
|
86
|
+
|
|
87
|
+
Operators who host independent tenants on one node must pair tenant isolation
|
|
88
|
+
with transport authentication, per-tenant quotas, audit review, and operational
|
|
89
|
+
resource controls.
|
|
90
|
+
|
|
91
|
+
## API Key Provisioning
|
|
92
|
+
|
|
93
|
+
Tenant ID is assigned when an API key is created. Keys created without an
|
|
94
|
+
explicit `tenant_id` use `default`.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"entity_uri": "service:payments",
|
|
99
|
+
"permissions": ["read", "write"],
|
|
100
|
+
"tenant_id": "acme-corp"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`GET /v1/me` returns the resolved identity and tenant ID. With the plugin
|
|
105
|
+
disabled, non-default keys resolve to `default`; with the plugin enabled, the
|
|
106
|
+
stored non-default tenant is preserved.
|
|
107
|
+
|
|
108
|
+
## Tenant Naming
|
|
109
|
+
|
|
110
|
+
`tenant_id` is an opaque string. Operators should use a stable URL-safe slug
|
|
111
|
+
such as `acme-corp`, `team-ml`, or `staging`. The node does not maintain a
|
|
112
|
+
separate tenant registry; tenant identity is implicit in key assignment.
|
|
113
|
+
|
|
114
|
+
## Non-Goals
|
|
115
|
+
|
|
116
|
+
- This feature does not make multi-tenancy part of the default supported
|
|
117
|
+
surface.
|
|
118
|
+
- This feature does not publish a signed plugin artifact.
|
|
119
|
+
- This feature does not define per-tenant billing policy.
|
|
120
|
+
- This feature does not provide tenant-aware non-default federation.
|
|
121
|
+
- This feature does not provide process-level resource isolation between
|
|
122
|
+
tenants.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Experimental multi-tenant plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .config import MultiTenantConfig
|
|
6
|
+
from .manifest import PLUGIN_NAME, plugin_manifest
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"PLUGIN_NAME",
|
|
10
|
+
"MultiTenantConfig",
|
|
11
|
+
"plugin_manifest",
|
|
12
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Configuration schema for the multi-tenant plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MultiTenantConfig(BaseModel):
|
|
12
|
+
"""Operator-controlled gate for non-default tenant resolution."""
|
|
13
|
+
|
|
14
|
+
enabled: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_config_from_env(environ: Mapping[str, str] | None = None) -> MultiTenantConfig:
|
|
18
|
+
"""Load multi-tenant plugin gates from environment variables."""
|
|
19
|
+
|
|
20
|
+
env = environ if environ is not None else os.environ
|
|
21
|
+
return MultiTenantConfig(
|
|
22
|
+
enabled=_env_bool(env, "STIGMEM_MULTI_TENANT_ENABLED"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _env_bool(env: Mapping[str, str], name: str) -> bool:
|
|
27
|
+
raw = env.get(name, "")
|
|
28
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Hook handlers for the multi-tenant plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
from stigmem_node.plugins import (
|
|
9
|
+
Allow,
|
|
10
|
+
Deny,
|
|
11
|
+
PluginContext,
|
|
12
|
+
PluginHealth,
|
|
13
|
+
PluginHealthStatus,
|
|
14
|
+
TenantContext,
|
|
15
|
+
)
|
|
16
|
+
from stigmem_node.tenant import TenantIdError, validate_tenant_id
|
|
17
|
+
|
|
18
|
+
from .config import load_config_from_env
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def config_validate(_ctx: PluginContext, **_: Any) -> Allow | Deny:
|
|
24
|
+
"""Validate operator gates before the plugin is registered."""
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
load_config_from_env()
|
|
28
|
+
except ValidationError as exc:
|
|
29
|
+
return Deny(f"invalid multi-tenant plugin config: {exc}")
|
|
30
|
+
return Allow()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tenant_resolve(_ctx: PluginContext, value: TenantContext, **kwargs: Any) -> TenantContext:
|
|
34
|
+
"""Resolve non-default tenants only when the plugin is explicitly enabled.
|
|
35
|
+
|
|
36
|
+
When ``STIGMEM_MULTI_TENANT_ENABLED`` is false, this hook returns the
|
|
37
|
+
inbound context unchanged. When enabled, it validates and normalizes the
|
|
38
|
+
candidate tenant ID before promoting the context to ``resolved``.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
config = load_config_from_env()
|
|
42
|
+
if not config.enabled:
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
identity = kwargs.get("identity")
|
|
46
|
+
tenant_id = getattr(identity, "tenant_id", None)
|
|
47
|
+
if not tenant_id:
|
|
48
|
+
tenant_id = value.metadata.get("source_tenant_id")
|
|
49
|
+
if not tenant_id:
|
|
50
|
+
return value
|
|
51
|
+
try:
|
|
52
|
+
normalized_tenant_id = validate_tenant_id(str(tenant_id))
|
|
53
|
+
except TenantIdError:
|
|
54
|
+
return value
|
|
55
|
+
return TenantContext(
|
|
56
|
+
tenant_id=normalized_tenant_id,
|
|
57
|
+
metadata={
|
|
58
|
+
**value.metadata,
|
|
59
|
+
"tenant_context_source": "resolved",
|
|
60
|
+
"resolved_by": "stigmem-plugin-multi-tenant",
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def pre_assert_authorize(_ctx: PluginContext, **_: Any) -> Allow:
|
|
66
|
+
"""Acknowledge core-enforced tenant write authorization.
|
|
67
|
+
|
|
68
|
+
Behavior is identical regardless of ``STIGMEM_MULTI_TENANT_ENABLED``:
|
|
69
|
+
this hook always returns ``Allow()`` because tenant isolation is enforced by
|
|
70
|
+
core write paths. The plugin contributes tenant resolution, not write
|
|
71
|
+
authorization.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
return Allow()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pre_recall_authorize(_ctx: PluginContext, **_: Any) -> Allow:
|
|
78
|
+
"""Acknowledge core-enforced tenant recall authorization.
|
|
79
|
+
|
|
80
|
+
Behavior is identical regardless of ``STIGMEM_MULTI_TENANT_ENABLED``:
|
|
81
|
+
this hook always returns ``Allow()`` because tenant isolation is enforced by
|
|
82
|
+
core recall paths. The plugin contributes tenant resolution, not recall
|
|
83
|
+
authorization.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
return Allow()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def recall_filter(_ctx: PluginContext, value: T, **_: Any) -> T:
|
|
90
|
+
"""Return recall/query results unchanged after core tenant filtering.
|
|
91
|
+
|
|
92
|
+
With the gate disabled or enabled, this hook is a pass-through because core
|
|
93
|
+
recall/query SQL already filters by the resolved request tenant.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def federation_outbound_filter(_ctx: PluginContext, value: T, **_: Any) -> T:
|
|
100
|
+
"""Return outbound federation batches unchanged after core tenant filtering.
|
|
101
|
+
|
|
102
|
+
With the gate disabled or enabled, this hook is a pass-through because
|
|
103
|
+
v0.9.0a8 node-level federation is pinned to the default tenant by core.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def migration_register(_ctx: PluginContext, value: T, **_: Any) -> T:
|
|
110
|
+
"""Return core/plugin migrations unchanged.
|
|
111
|
+
|
|
112
|
+
With the gate disabled or enabled, this hook is a pass-through.
|
|
113
|
+
Tenant columns remain part of the core storage compatibility schema so
|
|
114
|
+
existing single-tenant data keeps the `default` partition. The plugin owns
|
|
115
|
+
non-default tenant resolution and validation, not the storage columns.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def health_check(_ctx: PluginContext) -> PluginHealth:
|
|
122
|
+
"""Report scaffold health for registry lifecycle tests."""
|
|
123
|
+
|
|
124
|
+
return PluginHealth(
|
|
125
|
+
status=PluginHealthStatus.HEALTHY,
|
|
126
|
+
message="multi-tenant plugin scaffold registered",
|
|
127
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Plugin manifest factory for multi-tenant isolation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from stigmem_node.plugins import PluginManifest
|
|
6
|
+
|
|
7
|
+
from . import handlers
|
|
8
|
+
from .config import MultiTenantConfig
|
|
9
|
+
|
|
10
|
+
PLUGIN_NAME = "stigmem-plugin-multi-tenant"
|
|
11
|
+
PLUGIN_VERSION = "0.1.0"
|
|
12
|
+
REQUIRES_STIGMEM = ">=0.9.0a3"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plugin_manifest() -> PluginManifest:
|
|
16
|
+
"""Return the entry-point manifest consumed by ``stigmem.plugins`` discovery."""
|
|
17
|
+
|
|
18
|
+
return PluginManifest(
|
|
19
|
+
name=PLUGIN_NAME,
|
|
20
|
+
version=PLUGIN_VERSION,
|
|
21
|
+
requires_stigmem=REQUIRES_STIGMEM,
|
|
22
|
+
capabilities=frozenset(
|
|
23
|
+
{
|
|
24
|
+
"facts.read",
|
|
25
|
+
"facts.write",
|
|
26
|
+
"recall.read",
|
|
27
|
+
"federation.read",
|
|
28
|
+
"federation.write",
|
|
29
|
+
"identity.read",
|
|
30
|
+
"tenant.read",
|
|
31
|
+
"tenant.write",
|
|
32
|
+
"audit.emit",
|
|
33
|
+
"config.read",
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
hooks={
|
|
37
|
+
"config_validate": handlers.config_validate,
|
|
38
|
+
"tenant_resolve": handlers.tenant_resolve,
|
|
39
|
+
"pre_assert_authorize": handlers.pre_assert_authorize,
|
|
40
|
+
"pre_recall_authorize": handlers.pre_recall_authorize,
|
|
41
|
+
"recall_filter": handlers.recall_filter,
|
|
42
|
+
"federation_outbound_filter": handlers.federation_outbound_filter,
|
|
43
|
+
"migration_register": handlers.migration_register,
|
|
44
|
+
},
|
|
45
|
+
config_schema=MultiTenantConfig,
|
|
46
|
+
health_check=handlers.health_check,
|
|
47
|
+
async_safe=True,
|
|
48
|
+
)
|