personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(talents): pin access_tier/type against request override

prepare_config merged request values over the talent definition, and while
cwd was guarded, access_tier and type were not — a request body could raise
a talent's access_tier (which selects tool capability) or change its type
(which steers provider/model resolution), escalating privileges past the
talent definition.

Pin both from the definition, mirroring the cwd guard: a request value that
conflicts raises. Pin on presence, not just value — access_tier is populated
only for cogitate talents, so a request that introduces access_tier on a
talent that declares none is itself rejected (and no None key is injected).
Latent today (no shipped caller sets these), but the request arrives as a
JSON body and remote dispatch paths exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

+63
+27
solstone/think/talents.py
··· 566 566 talent_path = Path(config["path"]) if config.get("path") else None 567 567 sources = config.get("sources", {}) 568 568 talent_cwd = config.get("cwd") 569 + # Capture the security-relevant fields from the talent definition BEFORE the 570 + # request merge. access_tier selects tool capability; type steers provider/ 571 + # model resolution and the local-lane runtime promise. A request may not 572 + # override either (same as cwd). Pin on PRESENCE, not just value: 573 + # access_tier is populated only for cogitate talents (absent otherwise), so 574 + # a request that introduces access_tier on a talent that declares none is 575 + # itself the conflict to reject. 576 + definition_has_access_tier = "access_tier" in config 577 + definition_access_tier = config.get("access_tier") 578 + definition_type = config.get("type") 569 579 570 580 # Merge request values (request overrides talent defaults) 571 581 config.update({k: v for k, v in request.items() if v is not None}) ··· 574 584 raise ValueError( 575 585 f"Request overrides 'cwd' for talent '{name}' are not allowed " 576 586 f"({talent_cwd!r} != {request_cwd!r})" 587 + ) 588 + 589 + request_access_tier = request.get("access_tier") 590 + if request_access_tier is not None and ( 591 + not definition_has_access_tier 592 + or request_access_tier != definition_access_tier 593 + ): 594 + raise ValueError( 595 + f"Request overrides 'access_tier' for talent '{name}' are not allowed " 596 + f"({definition_access_tier!r} != {request_access_tier!r})" 597 + ) 598 + 599 + request_type = request.get("type") 600 + if request_type is not None and request_type != definition_type: 601 + raise ValueError( 602 + f"Request overrides 'type' for talent '{name}' are not allowed " 603 + f"({definition_type!r} != {request_type!r})" 577 604 ) 578 605 579 606 cwd_value = config.get("cwd")
+36
tests/test_prepare_config_pin.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """prepare_config pins security-relevant fields against request override. 5 + 6 + access_tier selects tool capability and type steers provider/model 7 + resolution; neither may be overridden by a request body (the same rule the 8 + cwd guard already enforces). The `partner` talent is type=cogitate with 9 + access_tier=synthesis, so a request tier of "normal" is a privilege 10 + escalation that must be refused. 11 + """ 12 + 13 + import pytest 14 + 15 + from solstone.think.talents import prepare_config 16 + 17 + 18 + def test_request_cannot_override_access_tier(): 19 + with pytest.raises(ValueError, match="access_tier"): 20 + prepare_config({"name": "partner", "access_tier": "normal"}) 21 + 22 + 23 + def test_request_cannot_override_type(): 24 + with pytest.raises(ValueError, match="type"): 25 + prepare_config({"name": "partner", "type": "generate"}) 26 + 27 + 28 + def test_matching_access_tier_is_a_noop(): 29 + config = prepare_config({"name": "partner", "access_tier": "synthesis"}) 30 + assert config["access_tier"] == "synthesis" 31 + 32 + 33 + def test_no_override_leaves_definition_values(): 34 + config = prepare_config({"name": "partner"}) 35 + assert config["access_tier"] == "synthesis" 36 + assert config["type"] == "cogitate"