Skip to content

Commit 79befbf

Browse files
committed
enforce memory_mode='framework': skip register_agent and AGENTS.md splice
RED-FIRST proof: ``` FAILED tests/test_memory_mode.py::TestMemoryModeFrameworkIsolation::test_framework_mode_skips_register_agent - AssertionError: Expected 'register_agent' to not have been called. Called 1 times. FAILED tests/test_memory_mode.py::TestMemoryModeFrameworkIsolation::test_framework_mode_skips_agents_md_taosmd_splice - AssertionError: taosmd rules block found in /root/.openclaw/AGENTS.md for framework-mode deploy 2 failed in 3.92s ``` After fix: ``` 2 passed in 3.92s ``` Changes: - tinyagentos/routes/agents.py: skip tm_agents.register_agent when memory_mode='framework' - tinyagentos/deployer.py: skip AGENTS.md taosmd splice when memory_mode='framework' - desktop/src/apps/agents/DeployWizard.tsx: hide Memory Layer controls for framework mode - docs/agent-coordination.md: update to say mode is now enforced - tests/test_memory_mode.py: add red-first tests for both behaviours - changelog.d: tsk-6tfpun-memory-mode-enforced.md
1 parent 1e9b63c commit 79befbf

6 files changed

Lines changed: 143 additions & 48 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Fixed
2+
- memory_mode 'framework' is now enforced: tm_agents.register_agent is skipped
3+
and AGENTS.md taosmd rules are not spliced when memory_mode='framework'.
4+
- Deploy wizard couples memoryMode to memoryPlugin and hides the Memory Layer
5+
controls when framework-only mode is selected.

desktop/src/apps/agents/DeployWizard.tsx

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,13 @@ export function DeployWizard({
426426
}
427427
}, [memoryPlugin]);
428428

429+
// When memory_mode is "framework", the taOSmd plugin must be disabled.
430+
useEffect(() => {
431+
if (memoryMode === "framework" && memoryPlugin !== null) {
432+
setMemoryPlugin(null);
433+
}
434+
}, [memoryMode]);
435+
429436
// Step 5 — Permissions
430437
const [canReadUserMemory, setCanReadUserMemory] = useState(false);
431438

@@ -1361,33 +1368,35 @@ export function DeployWizard({
13611368
{memoryMode === "taosmd" && "All memory goes to taOSmd. Durable and searchable across the fleet. Use this when the framework has no native memory."}
13621369
</div>
13631370

1364-
<div className="border-t border-white/5 pt-3">
1365-
<span className="block text-xs text-shell-text-secondary mb-2">Memory Layer</span>
1366-
<MemoryWizardStep
1367-
memoryPlugin={memoryPlugin}
1368-
setMemoryPlugin={setMemoryPlugin}
1369-
memoryDeviceId={memoryDeviceId}
1370-
setMemoryDeviceId={setMemoryDeviceId}
1371-
memoryTierId={memoryTierId}
1372-
setMemoryTierId={setMemoryTierId}
1373-
memoryDefault={memoryDefault}
1374-
setMemoryDefault={setMemoryDefault}
1375-
memoryInstallTargets={memoryInstallTargets}
1376-
setMemoryInstallTargets={setMemoryInstallTargets}
1377-
memoryDevicesLoaded={memoryDevicesLoaded}
1378-
setMemoryDevicesLoaded={setMemoryDevicesLoaded}
1379-
memorySetupTaskId={memorySetupTaskId}
1380-
setMemorySetupTaskId={setMemorySetupTaskId}
1381-
memorySetupState={memorySetupState}
1382-
setMemorySetupState={setMemorySetupState}
1383-
memorySetupMsg={memorySetupMsg}
1384-
setMemorySetupMsg={setMemorySetupMsg}
1385-
memorySetupError={memorySetupError}
1386-
setMemorySetupError={setMemorySetupError}
1387-
memoryPickerMode={memoryPickerMode}
1388-
setMemoryPickerMode={setMemoryPickerMode}
1389-
/>
1390-
</div>
1371+
{memoryMode !== "framework" && (
1372+
<div className="border-t border-white/5 pt-3">
1373+
<span className="block text-xs text-shell-text-secondary mb-2">Memory Layer</span>
1374+
<MemoryWizardStep
1375+
memoryPlugin={memoryPlugin}
1376+
setMemoryPlugin={setMemoryPlugin}
1377+
memoryDeviceId={memoryDeviceId}
1378+
setMemoryDeviceId={setMemoryDeviceId}
1379+
memoryTierId={memoryTierId}
1380+
setMemoryTierId={setMemoryTierId}
1381+
memoryDefault={memoryDefault}
1382+
setMemoryDefault={setMemoryDefault}
1383+
memoryInstallTargets={memoryInstallTargets}
1384+
setMemoryInstallTargets={setMemoryInstallTargets}
1385+
memoryDevicesLoaded={memoryDevicesLoaded}
1386+
setMemoryDevicesLoaded={setMemoryDevicesLoaded}
1387+
memorySetupTaskId={memorySetupTaskId}
1388+
setMemorySetupTaskId={setMemorySetupTaskId}
1389+
memorySetupState={memorySetupState}
1390+
setMemorySetupState={setMemorySetupState}
1391+
memorySetupMsg={memorySetupMsg}
1392+
setMemorySetupMsg={setMemorySetupMsg}
1393+
memorySetupError={memorySetupError}
1394+
setMemorySetupError={setMemorySetupError}
1395+
memoryPickerMode={memoryPickerMode}
1396+
setMemoryPickerMode={setMemoryPickerMode}
1397+
/>
1398+
</div>
1399+
)}
13911400
</div>
13921401
)}
13931402

docs/agent-coordination.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,14 +1245,9 @@ memory systems the framework runtime is told to use:
12451245
| `framework` | the framework's own memory only |
12461246
| `taosmd` | taOSmd only |
12471247

1248-
- **`framework` is ADVISORY today, not enforced.** The mode tells the agent
1249-
runtime what to use; it does **not** yet stop the controller from involving
1250-
taOSmd. A `framework`-mode deploy still registers the agent with taOSmd
1251-
(`routes/agents.py`) and still splices taOSmd rules into `AGENTS.md`
1252-
(`deployer.py`, gated on the agent FRAMEWORK, not on this field). So a taOSmd
1253-
outage can still block a `framework` deploy, and the agent still receives
1254-
taOSmd rules. **Do not choose `framework` expecting isolation from taOSmd.**
1255-
Tracked as `tsk-6tfpun`; this note comes out when the mode is enforced.
1248+
- `framework` is enforced at deploy time. When `memory_mode` is `framework`, the
1249+
controller skips `tm_agents.register_agent` and does not splice taOSmd rules
1250+
into `AGENTS.md`. The framework's own memory is used exclusively.
12561251
- `POST /api/agents/deploy` takes `memory_mode` on the body, defaulting to
12571252
`both`. It is persisted on the agent record and **injected into the agent's
12581253
environment as `TAOS_MEMORY_MODE`** at deploy time, so the runtime honours it

tests/test_memory_mode.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,84 @@ def test_both_mode_requires_explicit_store_selection(self):
320320
assert durable != working
321321
assert durable == "taOSmd"
322322
assert working == "framework"
323+
324+
325+
@pytest.mark.asyncio
326+
class TestMemoryModeFrameworkIsolation:
327+
"""memory_mode='framework' must actually exclude taOSmd from the deploy.
328+
329+
The bug: selecting 'framework' does not stop taOSmd from being involved.
330+
register_agent is called unconditionally, and AGENTS.md is spliced with
331+
taosmd rules regardless of memory_mode.
332+
"""
333+
334+
async def test_framework_mode_skips_register_agent(self, client, app):
335+
"""tm_agents.register_agent must NOT be called when memory_mode='framework'."""
336+
app.state.archive = MagicMock(
337+
record=AsyncMock(), query=AsyncMock(return_value=[{}])
338+
)
339+
with patch("tinyagentos.routes.agents.tm_agents.register_agent") as mock_reg:
340+
resp = await client.post("/api/agents/deploy", json={
341+
"name": "FW-No-Reg",
342+
"framework": "openclaw",
343+
"memory_mode": "framework",
344+
})
345+
assert resp.status_code == 200
346+
mock_reg.assert_not_called()
347+
348+
async def test_framework_mode_skips_agents_md_taosmd_splice(self, tmp_path):
349+
"""No taosmd block must be spliced into AGENTS.md when memory_mode='framework'."""
350+
from pathlib import Path
351+
from tinyagentos.deployer import deploy_agent, DeployRequest
352+
353+
pushed: list[tuple[str, str]] = []
354+
355+
async def fake_push_file(container, src, dst):
356+
try:
357+
with open(src) as fh:
358+
pushed.append((dst, fh.read()))
359+
except FileNotFoundError:
360+
pushed.append((dst, ""))
361+
return 0, ""
362+
363+
async def mock_exec(name, cmd, **kwargs):
364+
cmd_str = " ".join(cmd)
365+
if "hostname -I" in cmd_str:
366+
return (0, "10.0.0.5")
367+
return (0, "ok")
368+
369+
fake_rules = "Follow the taosmd librarian protocol.\nAgent: <your-agent-name>"
370+
import types
371+
import sys
372+
fake_taosmd = types.ModuleType("taosmd")
373+
fake_taosmd.agent_rules = lambda: fake_rules
374+
sys.modules["taosmd"] = fake_taosmd
375+
try:
376+
with patch("tinyagentos.deployer.create_container", new_callable=AsyncMock) as mock_create, \
377+
patch("tinyagentos.deployer.exec_in_container", side_effect=mock_exec), \
378+
patch("tinyagentos.deployer.push_file", side_effect=fake_push_file), \
379+
patch("tinyagentos.deployer.add_proxy_device", new_callable=AsyncMock, return_value={"success": True, "output": ""}):
380+
mock_create.return_value = {"success": True, "name": "taos-agent-fw-no-md"}
381+
req = DeployRequest(
382+
name="fw-no-md",
383+
framework="openclaw",
384+
model=None,
385+
data_dir=tmp_path,
386+
memory_mode="framework",
387+
)
388+
result = await deploy_agent(req)
389+
assert result["success"] is True
390+
finally:
391+
sys.modules.pop("taosmd", None)
392+
393+
agents_md_entries = [
394+
(dst, content) for dst, content in pushed
395+
if dst.endswith("AGENTS.md")
396+
]
397+
for dst, content in agents_md_entries:
398+
assert "<!-- taosmd:rules-begin -->" not in content, (
399+
f"taosmd rules block found in {dst} for framework-mode deploy"
400+
)
401+
assert "<!-- taosmd:rules-end -->" not in content, (
402+
f"taosmd rules block found in {dst} for framework-mode deploy"
403+
)

tinyagentos/deployer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,10 @@ async def deploy_agent(req: DeployRequest) -> dict:
682682
# sentinels so it never conflicts with a user-supplied template. If
683683
# the file already exists (from the Store or hand-crafted), we splice
684684
# only our block in; everything outside the sentinels is preserved.
685-
if req.framework in AGENTS_MD_PATHS:
685+
#
686+
# Skipped entirely when memory_mode='framework' because that mode
687+
# explicitly opts out of taOSmd.
688+
if req.framework in AGENTS_MD_PATHS and req.memory_mode != "framework":
686689
target_path = AGENTS_MD_PATHS[req.framework]
687690
try:
688691
import taosmd as _taosmd

tinyagentos/routes/agents.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -644,18 +644,20 @@ async def deploy_agent_endpoint(request: Request, body: DeployAgentRequest):
644644
idempotency_cache.set(scoped_key, json.loads(remote_err.body))
645645
return remote_err
646646

647-
# Register the agent with taosmd BEFORE mutating config so a failure
648-
# here aborts cleanly with no half-state.
649-
try:
650-
tm_agents.register_agent(unique_slug)
651-
except tm_agents.AgentExistsError:
652-
pass # idempotent — agent already registered, proceed normally
653-
except Exception as e:
654-
logger.exception("register_agent(%s) failed", unique_slug)
655-
err_body = {"error": f"Could not register agent with taosmd: {e}"}
656-
if scoped_key and idempotency_cache is not None:
657-
idempotency_cache.set(scoped_key, err_body)
658-
return JSONResponse(err_body, status_code=500)
647+
# Register the agent with taOSmd BEFORE mutating config so a failure
648+
# here aborts cleanly with no half-state. Skipped for memory_mode='framework'
649+
# because that mode explicitly opts out of taOSmd.
650+
if body.memory_mode != "framework":
651+
try:
652+
tm_agents.register_agent(unique_slug)
653+
except tm_agents.AgentExistsError:
654+
pass # idempotent — agent already registered, proceed normally
655+
except Exception as e:
656+
logger.exception("register_agent(%s) failed", unique_slug)
657+
err_body = {"error": f"Could not register agent with taosmd: {e}"}
658+
if scoped_key and idempotency_cache is not None:
659+
idempotency_cache.set(scoped_key, err_body)
660+
return JSONResponse(err_body, status_code=500)
659661

660662
# Register the agent in the agent registry, minting a canonical_id.
661663
# Every deploy creates a NEW agent entry (slugs are made unique above),

0 commit comments

Comments
 (0)