Skip to content

feat: Implement SelfModificationEngine with REST API (issue #95)#128

Merged
Steake merged 4 commits intomainfrom
copilot/implement-self-modification-engine
Mar 7, 2026
Merged

feat: Implement SelfModificationEngine with REST API (issue #95)#128
Steake merged 4 commits intomainfrom
copilot/implement-self-modification-engine

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 6, 2026

Description

The self-modification UI (PR #41) had no backend engine. This PR implements SelfModificationEngine, wires it into the server lifespan, and exposes a REST API the UI can consume.

backend/core/self_modification_engine.py (new)

  • Fully in-memory engine with propose/apply/rollback semantics and ordered history
  • 4 targets: knowledge_graph, inference_rules, attention_weights, active_modules
  • Operations: add, remove, modify, enable, disable
  • Before/after snapshots stored per record — rollback is fully deterministic
  • asyncio.Lock guards all state mutations for concurrent safety
  • History retention capped at 1000 records with automatic eviction of oldest entries
  • Custom exceptions ProposalNotFoundError (→ HTTP 404) and ProposalStateError (→ HTTP 409) for precise error semantics
  • Input validation: parameters must be a dict; attention_weights requires non-empty component and valid numeric weight; active_modules enable/disable/remove require non-empty module identifier
engine = SelfModificationEngine()
p = await engine.propose_modification("knowledge_graph", "add", {"concept": "Testability"})
r = await engine.apply_modification(p.proposal_id)   # r.status == "applied"
await engine.rollback_modification(p.proposal_id)    # restores pre-apply snapshot

backend/unified_server.py

  • self_modification_engine = None defined at module scope; initialized in lifespan startup with explicit None on failure
  • Session-token authentication via X-API-Token header on all self-modification endpoints; reads GODELOS_API_TOKEN env var; auth skipped when unset (dev mode)
  • 4 new endpoints:
    • POST /api/self-modification/propose{proposal_id, status: "pending", ...}
    • POST /api/self-modification/apply/{id}{status: "applied", changes_summary} (409 on double-apply)
    • POST /api/self-modification/rollback/{id}{status: "rolled_back"} (409 on double-rollback)
    • GET /api/self-modification/history → ordered record list
  • GET /api/system/modules — returns active modules registry from the engine
  • GET /api/knowledge — merges engine's knowledge items into the response, respecting knowledge_type filter and limit parameter
  • parameters validated at propose-time: null coerced to {}; non-object returns 400

backend/core/__init__.py

  • Wrapped eager imports in try/except with logger.warning() — prevents a missing numpy (or similar optional dep) from propagating as an ImportError while still logging the cause for diagnostics

tests/backend/test_self_modification.py (new)

28 tests across unit and HTTP layers covering all 4 required scenarios plus edge cases:

  • Auth enforcement (401 without token, 200 with correct token)
  • Invalid target → 400, double-apply → 409, double-rollback → 409, rollback-before-apply → 404
  • Null/non-object parameters → 400, empty module name → 400, invalid weight → 400
  • History pruning when exceeding retention cap

Related Issues

Test Evidence

tests/backend/test_self_modification.py ............................ 28 passed in 0.56s

Checklist

  • Tests pass locally (pytest tests/)
  • Code is formatted (black . and isort .)
  • Documentation updated (if applicable)
  • No secrets or credentials committed
  • Related issue linked above
Original prompt

Context — Issue #95

PR #41 implements a self-modification UI in the frontend, but the backend engine that the UI is intended to drive is either dormant or absent. This issue implements the SelfModificationEngine, wires it into the server, and exposes it via a REST API consumed by the existing UI.

Tasks

1. Audit existing code

  • Search godelOS/ for any existing self-modification primitives (e.g. SelfModificationEngine, ArchitecturalSelfModifier, rule-mutation utilities, weight-update hooks)
  • If found: activate and extend. If absent: implement from scratch.

2. Implement SelfModificationEngine

Create backend/core/self_modification_engine.py (or activate the dormant equivalent) with:

class SelfModificationEngine:
    async def propose_modification(self, target: str, operation: str, parameters: dict) -> ModificationProposal
    async def apply_modification(self, proposal_id: str) -> ModificationResult
    async def rollback_modification(self, proposal_id: str) -> RollbackResult
    async def get_history(self) -> list[ModificationRecord]
  • target can be: "knowledge_graph", "inference_rules", "attention_weights", "active_modules"
  • Operations include: "add", "remove", "modify", "enable", "disable"
  • All applied modifications must be tracked in an in-memory history log (with timestamps, proposal IDs, before/after snapshots)
  • Rollback must restore the before-snapshot for the given proposal
  • Modifications to knowledge_graph must produce verifiable changes retrievable via GET /api/knowledge
  • Modifications to active_modules must produce verifiable changes retrievable via GET /api/system/modules (if that endpoint exists) or a modules status dict

3. Expose REST API

Add to backend/unified_server.py (or appropriate router):

Method Path Description
POST /api/self-modification/propose Submit a modification proposal; returns { proposal_id, target, operation, parameters, status: "pending" }
POST /api/self-modification/apply/{proposal_id} Apply a pending proposal; returns { proposal_id, status: "applied", changes_summary }
POST /api/self-modification/rollback/{proposal_id} Roll back an applied modification; returns { proposal_id, status: "rolled_back" }
GET /api/self-modification/history Returns ordered list of all modification records

4. Wire into startup

  • Initialise SelfModificationEngine in the server lifespan/startup event
  • Inject into router via app.state or dependency injection

5. Tests

  • test_propose_modification: POST propose → 200, proposal_id in response
  • test_apply_modification: propose + apply → GET /api/knowledge (or modules) shows the change
  • test_rollback_modification: apply + rollback → change is reversed
  • test_history: history endpoint returns records in order, including both applied and rolled-back entries

Acceptance Criteria

  • Self-modification operations initiated via the API produce verifiable changes to the knowledge graph or active reasoning modules
  • Rollback fully reverses an applied modification
  • All 4 tests pass

Related Issue

Fixes #95

This pull request was created from Copilot chat.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement SelfModificationEngine and connect to server feat: Implement SelfModificationEngine with REST API (issue #95) Mar 6, 2026
@Steake Steake marked this pull request as ready for review March 7, 2026 04:51
@Steake Steake self-requested a review as a code owner March 7, 2026 04:51
Copilot AI review requested due to automatic review settings March 7, 2026 04:51
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

🧪 CI — Python 3.11

�[31mERROR�[0m tests/test_cognitive_subsystem_activation.py::�[1mTestEndToEndFlow::test_context_engine_round_trip�[0m - ModuleNotFoundError: No module named 'requests'
�[31mERROR�[0m tests/test_cognitive_subsystem_activation.py::�[1mTestEndToEndFlow::test_nlg_pipeline_process�[0m - ModuleNotFoundError: No module named 'requests'
�[31m====== �[31m�[1m4 failed�[0m, �[32m980 passed�[0m, �[33m81 skipped�[0m, �[33m65 warnings�[0m, �[31m�[1m22 errors�[0m�[31m in 39.30s�[0m�[31m ======�[0m

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

🧪 CI — Python 3.10

�[31mERROR�[0m tests/test_cognitive_subsystem_activation.py::�[1mTestEndToEndFlow::test_context_engine_round_trip�[0m - ModuleNotFoundError: No module named 'requests'
�[31mERROR�[0m tests/test_cognitive_subsystem_activation.py::�[1mTestEndToEndFlow::test_nlg_pipeline_process�[0m - ModuleNotFoundError: No module named 'requests'
�[31m====== �[31m�[1m4 failed�[0m, �[32m980 passed�[0m, �[33m81 skipped�[0m, �[33m65 warnings�[0m, �[31m�[1m22 errors�[0m�[31m in 34.03s�[0m�[31m ======�[0m

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements an in-memory SelfModificationEngine and exposes it via new REST endpoints in backend/unified_server.py so the existing self-modification UI (issue #95 / PR #41) can propose/apply/rollback changes and verify them via API reads.

Changes:

  • Added SelfModificationEngine with propose/apply/rollback and ordered history tracking.
  • Wired the engine into unified_server lifespan and added self-modification + modules status endpoints; /api/knowledge now merges engine knowledge items.
  • Added backend tests covering engine behavior plus HTTP endpoint flows.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
backend/core/self_modification_engine.py New in-memory engine implementing propose/apply/rollback across multiple targets with snapshot-based rollback and history.
backend/unified_server.py Initializes the engine on startup; adds self-modification endpoints; merges engine knowledge items into /api/knowledge; adds /api/system/modules.
backend/core/__init__.py Wraps eager imports to avoid package-level import failures when optional deps are missing.
tests/backend/test_self_modification.py Adds unit + HTTP tests for proposing, applying, rolling back, and reading modification history.

Comment on lines +4022 to +4027
engine_items = self_modification_engine.get_knowledge_items()
if engine_items:
existing_concepts = list(base.get("concepts", []))
existing_concepts.extend(engine_items)
result = {**base, "concepts": existing_concepts, "total_count": int(base.get("total_count", 0)) + len(engine_items)}
return result
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge logic appends engine concepts regardless of knowledge_type and limit, so /api/knowledge?knowledge_type=fact&limit=50 can still return concept entries and exceed the requested limit. Filter/limit the engine-provided items consistently with the endpoint’s query params (or gate the merge to only concept queries).

Suggested change
engine_items = self_modification_engine.get_knowledge_items()
if engine_items:
existing_concepts = list(base.get("concepts", []))
existing_concepts.extend(engine_items)
result = {**base, "concepts": existing_concepts, "total_count": int(base.get("total_count", 0)) + len(engine_items)}
return result
# Only merge engine-provided items when concepts are requested (or no specific type is given)
if knowledge_type is None or str(knowledge_type).lower() in {"concept", "concepts"}:
engine_items = self_modification_engine.get_knowledge_items()
if engine_items:
existing_concepts = list(base.get("concepts", []))
added_items = 0
# Respect the limit parameter for the total number of concept items
if isinstance(limit, int) and limit > 0:
remaining_slots = max(limit - len(existing_concepts), 0)
if remaining_slots > 0:
to_add = list(engine_items)[:remaining_slots]
merged_concepts = existing_concepts + to_add
added_items = len(to_add)
else:
merged_concepts = existing_concepts
else:
merged_concepts = existing_concepts + list(engine_items)
added_items = len(engine_items)
result = {
**base,
"concepts": merged_concepts,
"total_count": int(base.get("total_count", 0)) + added_items,
}
return result

Copilot uses AI. Check for mistakes.
Comment on lines +4109 to +4112
try:
result = await self_modification_engine.apply_modification(proposal_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint maps all ValueErrors from apply_modification() to HTTP 404. The engine raises ValueError for non-not-found cases too (e.g., proposal exists but status is not pending, or parameters are invalid), so callers will get a misleading 404. Consider distinguishing not-found vs invalid-state/validation errors (e.g., 404 vs 409/400) by using dedicated exception types or inspecting the error cause.

Copilot uses AI. Check for mistakes.
Comment on lines +4082 to +4088
target = payload.get("target", "")
operation = payload.get("operation", "")
parameters = payload.get("parameters", {})
try:
proposal = await self_modification_engine.propose_modification(
target=target, operation=operation, parameters=parameters
)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters is passed through without shape validation. If a client sends "parameters": null (or another non-object), the engine will later crash on .get(...) during apply, returning a 500. Validate that parameters is a dict/object at propose-time (or coerce None to {}) and return a 400 on invalid payloads.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +20
try:
from .cognitive_manager import CognitiveManager, get_cognitive_manager
from .agentic_daemon_system import AgenticDaemonSystem, get_agentic_daemon_system

__all__ = [
"CognitiveManager",
"get_cognitive_manager",
"AgenticDaemonSystem",
"get_agentic_daemon_system"
]
__all__ = [
"CognitiveManager",
"get_cognitive_manager",
"AgenticDaemonSystem",
"get_agentic_daemon_system",
]
except ImportError:
__all__ = []
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backend.core.__init__ now swallows any ImportError from these imports and silently exposes an empty __all__, which can mask real bugs (not just optional deps) and make failures harder to diagnose. Consider logging the exception (or re-raising unless the missing module is one of the known-optional deps), so broken imports don’t fail silently.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +104
def __init__(self) -> None:
# Per-target in-memory stores
self._knowledge_graph: Dict[str, Dict[str, Any]] = {}
self._inference_rules: Dict[str, Dict[str, Any]] = {}
self._attention_weights: Dict[str, float] = {}
self._active_modules: Dict[str, Dict[str, Any]] = copy.deepcopy(self._DEFAULT_MODULES)

# Proposal / history stores
self._proposals: Dict[str, ModificationProposal] = {}
self._records: Dict[str, ModificationRecord] = {}
self._history_order: List[str] = [] # ordered proposal IDs

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The engine mutates shared in-memory dicts (_proposals, _records, target stores) from async methods without any locking. Under concurrent requests, propose/apply/rollback can interleave and corrupt snapshots/history ordering. Consider guarding state mutations with an asyncio.Lock (as done in other core components) to make operations atomic.

Copilot uses AI. Check for mistakes.
Comment on lines +637 to +644
# Initialize SelfModificationEngine
try:
from backend.core.self_modification_engine import SelfModificationEngine
self_modification_engine = SelfModificationEngine()
logger.info("✅ SelfModificationEngine initialized")
except Exception as e:
logger.error(f"Failed to initialize SelfModificationEngine: {e}")

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self_modification_engine is assigned only inside the lifespan startup. If initialization fails, the name may never be bound, and later route handlers that reference it will raise NameError/AttributeError rather than returning 503. Define self_modification_engine = None at module scope and set it explicitly to None in the exception path here (optionally log with logger.exception).

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +104
# Proposal / history stores
self._proposals: Dict[str, ModificationProposal] = {}
self._records: Dict[str, ModificationRecord] = {}
self._history_order: List[str] = [] # ordered proposal IDs

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

History/proposal storage is unbounded and stores deep-copied before/after snapshots per apply, so memory usage will grow without limit on a long-running server. Consider adding a retention policy (max records), optional snapshot disabling, or a way to prune proposals/records after some TTL.

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +348
if operation in ("add", "modify"):
weight = float(parameters.get("weight", 0.5))
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

component can be empty and weight coercion via float(...) can raise TypeError/ValueError (e.g., null or non-numeric strings). Add validation that component is non-empty and weight is a valid number, and raise a clear validation error so the API can return 400 rather than failing unexpectedly.

Suggested change
if operation in ("add", "modify"):
weight = float(parameters.get("weight", 0.5))
if not component:
raise ValueError(
"Parameter 'component' is required for attention_weights operations"
)
if operation in ("add", "modify"):
raw_weight = parameters.get("weight", 0.5)
try:
weight = float(raw_weight)
except (TypeError, ValueError):
raise ValueError(
f"Invalid 'weight' value for attention_weights: {raw_weight!r}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +43
original = srv.self_modification_engine
srv.self_modification_engine = engine
yield app, engine
srv.self_modification_engine = original
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture assumes backend.unified_server.self_modification_engine exists at import time (original = srv.self_modification_engine). In the current server code it’s only created during lifespan startup, so this access can raise AttributeError and make the test suite order-dependent. Use getattr(srv, "self_modification_engine", None) and/or ensure the server defines the global as None at module import time.

Suggested change
original = srv.self_modification_engine
srv.self_modification_engine = engine
yield app, engine
srv.self_modification_engine = original
original_exists = hasattr(srv, "self_modification_engine")
original = getattr(srv, "self_modification_engine", None)
srv.self_modification_engine = engine
yield app, engine
if original_exists:
srv.self_modification_engine = original
else:
# Remove the attribute if we created it in this fixture
if hasattr(srv, "self_modification_engine"):
delattr(srv, "self_modification_engine")

Copilot uses AI. Check for mistakes.
Comment on lines +4074 to +4081
@app.post("/api/self-modification/propose")
async def propose_modification(payload: Dict[str, Any]):
"""Submit a modification proposal.

Returns ``{ proposal_id, target, operation, parameters, status: "pending" }``
"""
if self_modification_engine is None:
raise HTTPException(status_code=503, detail="SelfModificationEngine not initialized")
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These endpoints allow runtime self-modification (including enabling/disabling modules) with no authentication/authorization checks. If the server can be reached by untrusted clients, this is a high-impact security risk. Consider gating the self-modification routes behind an environment-flag (dev-only) or requiring an admin token/header before allowing propose/apply/rollback.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO this is the point of godelos, but we should have authenticated session for them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added session-token auth in a52067a. All self-modification endpoints now require a valid X-API-Token header when GODELOS_API_TOKEN is set. Auth is skipped when the env var is empty/unset (dev mode).

@Steake
Copy link
Copy Markdown
Owner

Steake commented Mar 7, 2026

@copilot apply changes based on the comments in this thread

- Add asyncio.Lock for concurrent safety on all state mutations
- Add history retention cap (max 1000 records) with eviction
- Add ProposalNotFoundError/ProposalStateError for 404 vs 409 mapping
- Validate parameters is dict, non-empty component/weight, non-empty module name
- Add session-token auth (X-API-Token header, GODELOS_API_TOKEN env var)
- Respect knowledge_type and limit in /api/knowledge merge
- Log ImportError in backend/core/__init__.py
- Define self_modification_engine=None at module scope
- Fix test fixture with getattr; disable lifespan in transport
- Add 11 new tests (28 total): auth, 409 conflict, validation edge cases

Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot apply changes based on the comments in this thread

All 12 review comments addressed in a52067a:

  • asyncio.Lock guards all state mutations
  • History capped at 1000 records with eviction
  • ProposalNotFoundError/ProposalStateError → 404 vs 409
  • parameters validated as dict; null coerced to {}; non-object → 400
  • Non-empty module name required for enable/disable/remove; non-empty component + valid weight for attention_weights
  • Session-token auth via X-API-Token header (reads GODELOS_API_TOKEN env var; skipped when unset)
  • /api/knowledge merge respects knowledge_type and limit
  • ImportError logged in backend/core/__init__.py
  • self_modification_engine = None at module scope
  • Test fixture uses getattr; transport disables lifespan
  • 28 tests total, all passing

@Steake Steake merged commit 751780f into main Mar 7, 2026
2 checks passed
@Steake
Copy link
Copy Markdown
Owner

Steake commented Mar 7, 2026

@copilot apply changes based on the comments in this thread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Self-modification engine — backend wiring and API

3 participants