bantamkit 0.27.0__py3-none-any.whl
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.
- bantamkit/__init__.py +32 -0
- bantamkit/agent.py +458 -0
- bantamkit/assets/contracts/default.yaml +90 -0
- bantamkit/assets/evals/devteam/manifest.yaml +351 -0
- bantamkit/assets/evals/devteam/repo/HISTORY.md +18 -0
- bantamkit/assets/evals/devteam/repo/README.md +12 -0
- bantamkit/assets/evals/devteam/repo/docs/architecture.md +17 -0
- bantamkit/assets/evals/devteam/repo/docs/runbook.md +10 -0
- bantamkit/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- bantamkit/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- bantamkit/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- bantamkit/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- bantamkit/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- bantamkit/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- bantamkit/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- bantamkit/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- bantamkit/assets/evals/fixtures/.gitkeep +0 -0
- bantamkit/assets/evals/fixtures/catalog.json +6 -0
- bantamkit/assets/evals/perturbations/task-completion.yaml +576 -0
- bantamkit/assets/evals/tasks/.gitkeep +0 -0
- bantamkit/assets/evals/tasks/extract-contact.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-invoice.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-order.yaml +15 -0
- bantamkit/assets/evals/tasks/extract-schedule.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-versions.yaml +17 -0
- bantamkit/assets/evals/tasks/nav-prod-port.yaml +84 -0
- bantamkit/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- bantamkit/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-db-port.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-deploy.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- bantamkit/assets/evals/tasks/recall-oncall.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-org-quota.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-owner.yaml +13 -0
- bantamkit/assets/evals/tasks/shop-basket-total.yaml +10 -0
- bantamkit/assets/evals/tasks/shop-cheapest.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-compare.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-stock-total.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-total.yaml +9 -0
- bantamkit/assets/profiles/default.yaml +31 -0
- bantamkit/assets/profiles/patient.yaml +31 -0
- bantamkit/assets/rubrics/.gitkeep +0 -0
- bantamkit/assets/rubrics/code-quality.yaml +20 -0
- bantamkit/assets/rubrics/grounded-completion.yaml +37 -0
- bantamkit/assets/rubrics/task-completion.yaml +28 -0
- bantamkit/assets/schemas/shiftwork-checkpoint.json +188 -0
- bantamkit/assets/skills/.gitkeep +0 -0
- bantamkit/assets/skills/file-graph.md +7 -0
- bantamkit/assets/skills/memory.md +35 -0
- bantamkit/assets/tools/.gitkeep +0 -0
- bantamkit/assets/tools/bantamkit_read.json +48 -0
- bantamkit/assets/tools/bantamkit_status.json +25 -0
- bantamkit/assets/tools/build_identity.json +17 -0
- bantamkit/assets/tools/document_list.json +12 -0
- bantamkit/assets/tools/document_read.json +31 -0
- bantamkit/assets/tools/file_graph.json +12 -0
- bantamkit/assets/tools/memory_compact.json +31 -0
- bantamkit/assets/tools/memory_recall.json +38 -0
- bantamkit/assets/tools/memory_save.json +61 -0
- bantamkit/assets/tools/shiftwork_clock_in.json +25 -0
- bantamkit/assets/tools/shiftwork_clock_out.json +60 -0
- bantamkit/assets/tools/shiftwork_status.json +25 -0
- bantamkit/assets/tools/skill_audit.json +70 -0
- bantamkit/assets/tools/validate_json.json +31 -0
- bantamkit/assets.py +67 -0
- bantamkit/budget.py +114 -0
- bantamkit/client.py +329 -0
- bantamkit/contract.py +522 -0
- bantamkit/criticreplay.py +3241 -0
- bantamkit/critique.py +301 -0
- bantamkit/docread.py +1744 -0
- bantamkit/evalrun.py +2003 -0
- bantamkit/eventlog.py +282 -0
- bantamkit/filegraph.py +218 -0
- bantamkit/loopguard.py +101 -0
- bantamkit/mcpreport.py +763 -0
- bantamkit/mcpserver.py +1334 -0
- bantamkit/memory/__init__.py +28 -0
- bantamkit/memory/__main__.py +291 -0
- bantamkit/memory/component.py +569 -0
- bantamkit/memory/divergence.py +744 -0
- bantamkit/memory/layers.py +257 -0
- bantamkit/memory/store.py +940 -0
- bantamkit/pdfread.py +1402 -0
- bantamkit/profile.py +46 -0
- bantamkit/shiftwork.py +212 -0
- bantamkit/skillaudit.py +853 -0
- bantamkit/statusline.py +313 -0
- bantamkit/structured.py +125 -0
- bantamkit/textutil.py +30 -0
- bantamkit-0.27.0.dist-info/METADATA +207 -0
- bantamkit-0.27.0.dist-info/RECORD +119 -0
- bantamkit-0.27.0.dist-info/WHEEL +4 -0
- bantamkit-0.27.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Layer resolution: project-store discovery and explicit cross-project grants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import stat as stat_module
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from bantamkit.memory.store import MemoryValidationError
|
|
14
|
+
|
|
15
|
+
PROJECT_STORE = Path(".bantamkit") / "memory"
|
|
16
|
+
CONFIG_NAME = "config.yaml"
|
|
17
|
+
|
|
18
|
+
# The operator's one lever over binding, and the only one they actually hold: an
|
|
19
|
+
# MCP server's cwd is chosen by the HOST, not by the user, so every rule derived
|
|
20
|
+
# from cwd is a rule the operator cannot control. The launch environment is the
|
|
21
|
+
# surface every host exposes — `{"command": ..., "env": {"BANTAMKIT_MEMORY_DIR":
|
|
22
|
+
# "/abs/path/to/store"}}`. The name is spelled here and nowhere else, including
|
|
23
|
+
# in the tests, because a magic string duplicated between a resolver and its test
|
|
24
|
+
# is how the two come to be reading different variables.
|
|
25
|
+
MEMORY_DIR_ENV = "BANTAMKIT_MEMORY_DIR"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class StoreBinding:
|
|
30
|
+
"""Which store the walk bound to, and whether that store can answer at all.
|
|
31
|
+
|
|
32
|
+
`state` is one of three, and they are three because two of them were one:
|
|
33
|
+
|
|
34
|
+
- "populated" — the walk found a store and it holds facts.
|
|
35
|
+
- "empty" — the walk found a store and it holds none. A recall against it
|
|
36
|
+
returns nothing, and that nothing means "wrong filing cabinet",
|
|
37
|
+
not "no match".
|
|
38
|
+
- "designated" — the walk found no store anywhere up the tree. `path` is where
|
|
39
|
+
one WOULD go; nothing is there and nothing was created.
|
|
40
|
+
|
|
41
|
+
Measured 2026-08-22, and this is why the state is named here instead of being
|
|
42
|
+
re-derived at each call site: MCP server pid 34377 (cwd
|
|
43
|
+
/Users/kktest/Documents/Claude/Projects/trader-platform) walks past a project
|
|
44
|
+
with no store of its own and binds to ~/.bantamkit/memory — created
|
|
45
|
+
2026-08-10 22:15, 0 files in facts/, no index.md. Its `memory_recall` binds
|
|
46
|
+
successfully and returns the empty list a 64-fact store returns for a question
|
|
47
|
+
nothing matches. The same thing fires inside this repo's own linked worktrees:
|
|
48
|
+
a worktree has no `.bantamkit/` of its own, so
|
|
49
|
+
/Users/kktest/Documents/Claude/Projects/bantamkit-membind resolves to that same
|
|
50
|
+
empty home store while the canonical checkout resolves to its 64 facts. A
|
|
51
|
+
caller that only gets a path cannot tell those apart without re-running the
|
|
52
|
+
walk itself and hoping it reproduced it; `path` plus `state` is that answer.
|
|
53
|
+
|
|
54
|
+
`searched_from` is the resolved directory the walk started at, so a caller can
|
|
55
|
+
say why this store and not another without reconstructing the ascent. It is
|
|
56
|
+
`None` when no walk ran — see `origin`.
|
|
57
|
+
|
|
58
|
+
`origin` is "walk" or "pin". A pinned binding that still reported
|
|
59
|
+
`searched_from` would be asserting an ascent that never happened, and a
|
|
60
|
+
diagnostic field that lies is the same defect as a state field that lies.
|
|
61
|
+
Every consumer that wants to say WHY this store reads the pair.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
path: Path
|
|
65
|
+
state: str # "populated" | "empty" | "designated"
|
|
66
|
+
fact_count: int
|
|
67
|
+
searched_from: Path | None
|
|
68
|
+
origin: str = "walk" # "walk" | "pin"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def discover_project_store(start: str | Path | None = None) -> Path:
|
|
72
|
+
"""Walk up from `start` (default cwd) looking for an existing .bantamkit/memory.
|
|
73
|
+
|
|
74
|
+
Returns the nearest existing store dir; if none exists anywhere up the
|
|
75
|
+
tree, designates `start/.bantamkit/memory` without creating anything.
|
|
76
|
+
Ancestor path is fully resolved; the returned store path is not resolved
|
|
77
|
+
further — a symlinked store keeps its config beside the symlink. Callers
|
|
78
|
+
needing store identity comparison must resolve() at the comparison site.
|
|
79
|
+
|
|
80
|
+
`BANTAMKIT_MEMORY_DIR` outranks all of it — see `_pinned_store`. When a pin is
|
|
81
|
+
set, `start` is never consulted; when it is not, this is the walk it always was.
|
|
82
|
+
|
|
83
|
+
The path this returns does not move: `resolve_project_store` runs the same
|
|
84
|
+
walk and only adds what was found there.
|
|
85
|
+
"""
|
|
86
|
+
pin = _pinned_store()
|
|
87
|
+
if pin is not None:
|
|
88
|
+
return pin
|
|
89
|
+
return _walk_to_store(_resolved_base(start))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_project_store(start: str | Path | None = None) -> StoreBinding:
|
|
93
|
+
"""Same walk as `discover_project_store`, plus what is actually in the store.
|
|
94
|
+
|
|
95
|
+
Read-only in both directions: designating a path stays the non-destructive act
|
|
96
|
+
it already was, and an existing store is only listed, never touched.
|
|
97
|
+
|
|
98
|
+
Counted the way the store itself reads facts (`facts/*.md`, see
|
|
99
|
+
`MemoryStore._facts`), so a count of 0 here means recall has nothing to return,
|
|
100
|
+
not that the counting rule differs from the reading rule. An unreadable
|
|
101
|
+
`facts/` raises `MemoryValidationError` rather than reporting "empty" —
|
|
102
|
+
reporting a store you could not read as a store with nothing in it is the exact
|
|
103
|
+
conflation this exists to end, and it matches `load_grants` above, where a
|
|
104
|
+
grant that is wrong raises instead of being silently dropped. That raise is
|
|
105
|
+
`count_facts`' doing and only `count_facts`' doing: this docstring claimed it
|
|
106
|
+
for a release while `Path.glob` was quietly swallowing the error (measured: a
|
|
107
|
+
store holding one fact at mode 0o000 came back `state="empty", fact_count=0`).
|
|
108
|
+
|
|
109
|
+
A pin never yields "designated": `_pinned_store` has already established that
|
|
110
|
+
the pinned directory is there, or raised saying it is not.
|
|
111
|
+
"""
|
|
112
|
+
pin = _pinned_store()
|
|
113
|
+
if pin is not None:
|
|
114
|
+
return _count_into_binding(pin, searched_from=None, origin="pin")
|
|
115
|
+
base = _resolved_base(start)
|
|
116
|
+
path = _walk_to_store(base)
|
|
117
|
+
if not path.is_dir():
|
|
118
|
+
return StoreBinding(
|
|
119
|
+
path=path, state="designated", fact_count=0, searched_from=base, origin="walk"
|
|
120
|
+
)
|
|
121
|
+
return _count_into_binding(path, searched_from=base, origin="walk")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def count_facts(root: str | Path) -> int:
|
|
125
|
+
"""How many `facts/*.md` a store holds, counted so that unreadable is never zero.
|
|
126
|
+
|
|
127
|
+
`Path.glob` is unusable here, and that is the entire reason this function exists:
|
|
128
|
+
it suppresses the `OSError` raised by its own directory scan and yields nothing,
|
|
129
|
+
so `facts/` at mode 0o000 with a fact in it counts 0 — a store you could not read,
|
|
130
|
+
reported as a store with nothing in it. `os.scandir` raises, and each caller
|
|
131
|
+
decides what to do with the raise; nobody gets to be told "empty" by accident.
|
|
132
|
+
|
|
133
|
+
A `facts/` that is simply absent is 0 and not an error: nothing is stored there
|
|
134
|
+
and that much really is readable. The name filter mirrors `MemoryStore._facts`'
|
|
135
|
+
`glob("*.md")` exactly — dotfiles and directories included, as `glob` includes
|
|
136
|
+
them — because a count that disagrees with the read is the other half of the
|
|
137
|
+
same lie (`test_a_readable_store_is_still_counted_exactly_as_before`).
|
|
138
|
+
"""
|
|
139
|
+
facts = Path(root) / "facts"
|
|
140
|
+
try:
|
|
141
|
+
with os.scandir(facts) as entries:
|
|
142
|
+
return sum(1 for entry in entries if fnmatch.fnmatch(entry.name, "*.md"))
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _count_into_binding(path: Path, searched_from: Path | None, origin: str) -> StoreBinding:
|
|
148
|
+
try:
|
|
149
|
+
count = count_facts(path)
|
|
150
|
+
except OSError as e:
|
|
151
|
+
raise MemoryValidationError(
|
|
152
|
+
f"memory store is unreadable: {path / 'facts'}: {e.strerror}; a store that "
|
|
153
|
+
f"could not be listed is not a store with nothing in it, and answering "
|
|
154
|
+
f"'empty' here is the conflation this binding exists to end"
|
|
155
|
+
) from e
|
|
156
|
+
state = "populated" if count else "empty"
|
|
157
|
+
return StoreBinding(
|
|
158
|
+
path=path, state=state, fact_count=count, searched_from=searched_from, origin=origin
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _pinned_store() -> Path | None:
|
|
163
|
+
"""The store named by `MEMORY_DIR_ENV`, or None if the operator named none.
|
|
164
|
+
|
|
165
|
+
Three rulings live here, and each one is a choice about who gets blamed:
|
|
166
|
+
|
|
167
|
+
- A blank value is NOT a pin. A host that emits `"BANTAMKIT_MEMORY_DIR": ""`
|
|
168
|
+
has named no store, and failing there would break the walk for an operator
|
|
169
|
+
who pinned nothing. Same stance as `assets_root()` on `BANTAMKIT_ASSETS`.
|
|
170
|
+
- A relative pin RAISES. A path resolved against cwd is a pin whose meaning
|
|
171
|
+
depends on the exact thing the pin exists to escape. `~` is expanded first,
|
|
172
|
+
because an MCP host passes `env` verbatim with no shell to expand it, so the
|
|
173
|
+
tilde arrives literally and would otherwise fail as a nonexistent directory.
|
|
174
|
+
- A pin that is not there RAISES, and is never created. Creating a store
|
|
175
|
+
because someone typo'd a path is how an empty store came to answer for a
|
|
176
|
+
populated one in the first place; the raise is `load_grants`' stance below,
|
|
177
|
+
where a grant you wrote that is wrong is surfaced rather than dropped.
|
|
178
|
+
|
|
179
|
+
`os.stat` rather than `Path.is_dir()` on purpose. `is_dir()` swallows
|
|
180
|
+
PermissionError and answers False, which would report an operator's real store
|
|
181
|
+
as a typo — the walk lives with that (it must try many candidates and cannot
|
|
182
|
+
raise on each unreadable one), but a pin is a single path the operator named
|
|
183
|
+
out loud, so it gets the accurate reason. The path is not symlink-resolved,
|
|
184
|
+
matching the walk: a symlinked store keeps its config beside the symlink.
|
|
185
|
+
"""
|
|
186
|
+
raw = os.environ.get(MEMORY_DIR_ENV)
|
|
187
|
+
if raw is None or not raw.strip():
|
|
188
|
+
return None
|
|
189
|
+
pin = Path(raw).expanduser()
|
|
190
|
+
if not pin.is_absolute():
|
|
191
|
+
raise MemoryValidationError(
|
|
192
|
+
f"pinned memory store must be an absolute path, got {raw!r} "
|
|
193
|
+
f"({MEMORY_DIR_ENV}={raw}); a relative pin is resolved against a cwd "
|
|
194
|
+
f"the MCP host chose, which is what the pin exists to override"
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
info = os.stat(pin)
|
|
198
|
+
except OSError as e:
|
|
199
|
+
raise MemoryValidationError(
|
|
200
|
+
f"pinned memory store is unreachable: {pin}: {e.strerror} "
|
|
201
|
+
f"({MEMORY_DIR_ENV}={raw}); nothing was created"
|
|
202
|
+
) from e
|
|
203
|
+
if not stat_module.S_ISDIR(info.st_mode):
|
|
204
|
+
raise MemoryValidationError(
|
|
205
|
+
f"pinned memory store is not a directory: {pin} ({MEMORY_DIR_ENV}={raw})"
|
|
206
|
+
)
|
|
207
|
+
return pin
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _resolved_base(start: str | Path | None) -> Path:
|
|
211
|
+
return (Path(start) if start is not None else Path.cwd()).resolve()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _walk_to_store(base: Path) -> Path:
|
|
215
|
+
for d in (base, *base.parents):
|
|
216
|
+
candidate = d / PROJECT_STORE
|
|
217
|
+
if candidate.is_dir():
|
|
218
|
+
return candidate
|
|
219
|
+
return base / PROJECT_STORE
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_grants(project_store: str | Path) -> list[Path]:
|
|
223
|
+
"""Read extra read-only store paths from the config beside the project store.
|
|
224
|
+
|
|
225
|
+
Missing config -> no grants. A config that exists but is wrong — unparsable,
|
|
226
|
+
not a mapping, non-list/non-str `extra_stores`, or a listed path that is not
|
|
227
|
+
an existing directory — raises MemoryValidationError: a grant you wrote that
|
|
228
|
+
is wrong is a mistake to surface at construction, not silently drop.
|
|
229
|
+
Returned grant paths are fully resolved.
|
|
230
|
+
"""
|
|
231
|
+
config_path = Path(project_store).parent / CONFIG_NAME
|
|
232
|
+
if not config_path.exists():
|
|
233
|
+
return []
|
|
234
|
+
if not config_path.is_file():
|
|
235
|
+
raise MemoryValidationError(f"invalid memory config {config_path}: not a file")
|
|
236
|
+
try:
|
|
237
|
+
data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
238
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
|
|
239
|
+
raise MemoryValidationError(f"invalid memory config {config_path}: {e}") from e
|
|
240
|
+
if data is None:
|
|
241
|
+
return []
|
|
242
|
+
if not isinstance(data, dict):
|
|
243
|
+
raise MemoryValidationError(f"invalid memory config {config_path}: expected a mapping")
|
|
244
|
+
raw = data.get("extra_stores", [])
|
|
245
|
+
if not isinstance(raw, list) or not all(isinstance(p, str) for p in raw):
|
|
246
|
+
raise MemoryValidationError(
|
|
247
|
+
f"invalid memory config {config_path}: extra_stores must be a list of paths"
|
|
248
|
+
)
|
|
249
|
+
grants: list[Path] = []
|
|
250
|
+
for entry in raw:
|
|
251
|
+
resolved = (config_path.parent / entry).resolve()
|
|
252
|
+
if not resolved.is_dir():
|
|
253
|
+
raise MemoryValidationError(
|
|
254
|
+
f"granted store does not exist: {resolved} (from {config_path})"
|
|
255
|
+
)
|
|
256
|
+
grants.append(resolved)
|
|
257
|
+
return grants
|