Core agent improvements: - RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector - Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection - Rich conversation storage for notable turns; compact_conversation truncates long user messages - Task-type classifier (query/action/analysis/creative) for observation tagging - Nested sub-agent visibility: deep delegations now register against the main agent's manager Child safety (Gabriel profile): - child_safety.py: filtering, audit logging, prompt constants for restricted sessions - .kiro/specs/child-safety-profile: requirements, design, tasks specs - GABRIEL_BOT_PROPOSAL.md: initial proposal doc - Reduced context window (10 msgs) and tutor-mode identity for restricted users Telegram adapter: - Polling watchdog: auto-restarts updater if polling drops unexpectedly - get_me() with exponential-backoff retry on NetworkError at startup - Correct stop() ordering: signal watchdog before cancelling tasks Email / Gmail: - send_email: supports file attachments (attachments list param) - get_email: surfaces attachment metadata in response Scheduled tasks / weather: - Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively - New scheduled tasks and scheduler state persistence Discord: - adapters/discord/__init__.py scaffold - discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config) Infrastructure: - n8n workflow exports (garvis_webhook, content_pipeline variants) - memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs - UCS C240 migration plan doc - requirements.txt: new deps - .claude/settings.json, fix_hooks.py: hook/permission tuning
451 lines
18 KiB
Markdown
451 lines
18 KiB
Markdown
# Tasks — Child Safety Profile
|
||
|
||
Implementation order matters. Each task has a dependency note where relevant.
|
||
Tasks within a phase can be worked in parallel; phases must be completed in order.
|
||
|
||
---
|
||
|
||
## Phase 1 — Identity & Config (no code changes, unblocks everything else)
|
||
|
||
### TASK-01 — Add gabriel's Slack user ID to config
|
||
**File:** `config/adapters.local.yaml`
|
||
**What:** Add gabriel's Slack ID to `allowed_users` and `user_mapping`; add `child_safety` block.
|
||
**Requires:** Son's actual Slack user ID (parent to provide)
|
||
|
||
```yaml
|
||
# Under slack adapter settings:
|
||
allowed_users:
|
||
- <JORDANS_SLACK_USER_ID> # Jordan
|
||
- <GABRIELS_SLACK_USER_ID> # Gabriel
|
||
|
||
user_mapping:
|
||
slack_<JORDANS_SLACK_USER_ID>: "jordan"
|
||
slack_<GABRIELS_SLACK_USER_ID>: "gabriel"
|
||
|
||
# New top-level block:
|
||
child_safety:
|
||
restricted_users:
|
||
- gabriel
|
||
audit_retention_days: 365
|
||
```
|
||
|
||
Note: The Slack adapter has no allow-list today. `allowed_users` only takes effect after
|
||
TASK-01b adds the check to `adapters/slack/adapter.py`. Do both together.
|
||
|
||
**Acceptance:** Bot recognises Gabriel's Slack account and routes him to username `"gabriel"`.
|
||
All other Slack users (not in `allowed_users`) are silently ignored.
|
||
|
||
---
|
||
|
||
### TASK-02 — Create gabriel's user profile
|
||
**File:** `memory_workspace/users/gabriel.md` (new)
|
||
**What:** Write the per-user profile that gets injected into the system prompt.
|
||
**Depends on:** Nothing
|
||
|
||
Content to include:
|
||
- Age: 13
|
||
- Interests: gaming, horror games, Lua scripting, Roblox Studio, game design
|
||
- Learning style: hands-on, wants working code examples, short explanations before diving in
|
||
- Communication style: casual, encouraging, celebrate what he's building
|
||
- Current projects: Roblox horror game (update as known)
|
||
- Do NOT include guardrail rules here — those come from the injected block
|
||
|
||
**Acceptance:** Profile loads without error; content appears in system prompt during gabriel's session (verify via debug log).
|
||
|
||
---
|
||
|
||
## Phase 2 — Audit Logger
|
||
|
||
### TASK-03 — Implement `ChildAuditLogger`
|
||
**File:** `child_safety.py` (new — start with this class only)
|
||
**What:** Thread-safe, non-blocking JSONL audit logger for child user interactions.
|
||
**Depends on:** Nothing (standalone)
|
||
|
||
Implementation notes:
|
||
- Mirror the pattern from `observation/interaction_logger.py` exactly
|
||
- Write to `memory_workspace/audit/{username}/YYYY-MM-DD.jsonl`
|
||
- Create directory on first write (not at init, to avoid creating dirs for unused usernames)
|
||
- Single public method: `log(username, platform, action, filter_stage, filter_reason, message, response)`
|
||
- `action` values: `"allowed"` | `"blocked"` | `"flagged"`
|
||
- `cleanup_old_logs(retention_days)` — prune files older than retention window
|
||
|
||
**Acceptance:** Unit-testable in isolation. Call `log()` twice, verify two JSONL lines written
|
||
to the correct file path with correct schema.
|
||
|
||
---
|
||
|
||
## Phase 3 — Filtering Logic
|
||
|
||
### TASK-04 — Implement `ChildSafetyConfig`
|
||
**File:** `child_safety.py` (add to existing file)
|
||
**What:** Config loader that reads the `child_safety` block from `adapters.local.yaml`.
|
||
**Depends on:** TASK-01 (config block must exist)
|
||
|
||
```python
|
||
class ChildSafetyConfig:
|
||
restricted_users: list[str]
|
||
audit_retention_days: int
|
||
|
||
@classmethod
|
||
def from_yaml(cls, config_path: Path) -> "ChildSafetyConfig": ...
|
||
|
||
def is_restricted(self, username: str) -> bool: ...
|
||
```
|
||
|
||
**Acceptance:** `is_restricted("gabriel")` returns `True`; `is_restricted("cloe")` returns `False`.
|
||
|
||
---
|
||
|
||
### TASK-05 — Implement `ChildSafetyFilter` — input (preprocessor)
|
||
**File:** `child_safety.py` (add to existing file)
|
||
**What:** Intent-pattern input filter.
|
||
**Depends on:** TASK-03, TASK-04
|
||
|
||
Implementation notes:
|
||
- Compile all regex patterns once at class instantiation (not per-call)
|
||
- Pattern evaluation order: hard-block → context signals → conditional block → pass
|
||
- `preprocess(message: InboundMessage) -> tuple[InboundMessage | None, str | None]`
|
||
- Returns `(message, None)` → pass through
|
||
- Returns `(None, reply_text)` → block; caller sends reply_text directly
|
||
- Call `AuditLogger.log()` with `action="blocked"` on any block
|
||
- Call `AuditLogger.log()` with `action="allowed"` on pass (response field left null here —
|
||
audit logger will be called again in the postprocessor with the full response)
|
||
|
||
Hard-block patterns (always active):
|
||
```python
|
||
HARD_BLOCK_PATTERNS = [
|
||
r"\b(sex|porn|nude|naked|explicit)\b",
|
||
r"\bhow (do i|to|can i).{0,40}(kill|hurt|stab|shoot|harm).{0,30}(myself|yourself)\b",
|
||
r"\bhow (do i|to|can i).{0,40}(hurt|stab|kill|attack|beat up|harm).{0,30}(my |a )?(sister|brother|mom|dad|teacher|classmate|friend|kid|child|person|someone|people)\b",
|
||
r"\b(give me|what is|find).{0,30}(address|phone number|school|location).{0,30}(of|for)\b",
|
||
]
|
||
```
|
||
|
||
Game context signals (exempts from conditional blocks):
|
||
```python
|
||
GAME_CONTEXT_SIGNALS = [
|
||
r"\bin (my |the |a )?(game|roblox|studio|script|map|level|world|place)\b",
|
||
r"\b(lua|roblox|studio|npc|hitbox|raycast|humanoid|workspace|basepart|tool|part)\b",
|
||
r"\b(code|script|function|method|module|class|variable|loop|event|animate|tween)\b",
|
||
r"\b(damage|health|respawn|kill|destroy)\b.{0,30}\b(player|npc|enemy|mob|character|humanoid)\b",
|
||
r"\bhow (do i|to|can i) (make|get|set|add|create|implement|build|script)\b",
|
||
]
|
||
```
|
||
|
||
Conditional block patterns (only active when no game context signal):
|
||
```python
|
||
CONDITIONAL_BLOCK_PATTERNS = [
|
||
r"\bhow (do i|to|can i).{0,40}(use|wield|make|build).{0,30}(knife|gun|pistol|rifle|weapon|sword|bomb).{0,30}(hurt|harm|attack|fight|cut|stab|shoot)\b",
|
||
r"\bhow (do i|to|can i).{0,40}(hurt|fight|attack|beat).{0,30}(someone|people|person|kid|child)\b",
|
||
r"\b(buy|get|obtain|find).{0,30}(drugs?|weed|cocaine|meth|pills)\b",
|
||
]
|
||
```
|
||
|
||
**Acceptance:**
|
||
- `"how do I make a knife swing animation in Roblox"` → pass (game context)
|
||
- `"how do I use a knife to hurt someone"` → block (no game context, conditional pattern)
|
||
- `"how do I kill all NPCs in my game"` → pass (game context signal: NPC)
|
||
- `"how do I hurt my classmate"` → block (hard block)
|
||
- `"lua script for a gun that shoots"` → pass (game context)
|
||
|
||
---
|
||
|
||
### TASK-06 — Implement `ChildSafetyFilter` — output (postprocessor)
|
||
**File:** `child_safety.py` (add to existing file)
|
||
**What:** Light response scan before delivery.
|
||
**Depends on:** TASK-05
|
||
|
||
Implementation notes:
|
||
- `postprocess(response: str, message: InboundMessage) -> str`
|
||
- Scan for: explicit terms list, profanity list, real-world harm instruction patterns
|
||
- If flagged: log `action="flagged"`, return safe fallback string
|
||
- If clean: log `action="allowed"` with full response (this is the final audit entry)
|
||
- Fallback string: "I ran into a bit of a snag there. Try rephrasing, or ask me something
|
||
about your Roblox game — I love helping with scripts and game design!"
|
||
|
||
**Acceptance:**
|
||
- Clean Lua code response → returned unchanged
|
||
- Response containing explicit term → replaced with fallback
|
||
|
||
---
|
||
|
||
## Phase 4 — System Prompt Injection
|
||
|
||
### TASK-07 — Add guardrail block injection to `agent.py`
|
||
**File:** `agent.py`, `_build_system_prompt()` (~line 488)
|
||
**What:** Inject `CHILD_GUARDRAIL_BLOCK` for restricted users.
|
||
**Depends on:** TASK-04
|
||
|
||
Changes:
|
||
1. Import `ChildSafetyConfig` and `CHILD_GUARDRAIL_BLOCK` from `child_safety`
|
||
2. In `Agent.__init__()`, load config and store as `self._child_safety_config`
|
||
3. In `_build_system_prompt()`, after the user profile block:
|
||
|
||
```python
|
||
if (not self.is_sub_agent
|
||
and self._child_safety_config
|
||
and self._child_safety_config.is_restricted(username)):
|
||
system_parts.append(CHILD_GUARDRAIL_BLOCK)
|
||
```
|
||
|
||
`CHILD_GUARDRAIL_BLOCK` is a module-level constant in `child_safety.py`.
|
||
|
||
**Acceptance:** Add a debug print temporarily — verify the guardrail block appears in the
|
||
assembled system prompt when username is `"gabriel"` and does not appear for `"cloe"`.
|
||
|
||
---
|
||
|
||
## Phase 5 — Runtime Wiring
|
||
|
||
### TASK-08 — Register pre/postprocessors in `adapters/runtime.py`
|
||
**File:** `adapters/runtime.py`, `AdapterRuntime.__init__()`
|
||
**What:** Instantiate the filter and register it with the runtime.
|
||
**Depends on:** TASK-05, TASK-06
|
||
|
||
```python
|
||
from child_safety import ChildSafetyConfig, ChildSafetyFilter, ChildAuditLogger
|
||
|
||
_cs_config = ChildSafetyConfig.from_yaml(config_path)
|
||
_cs_audit = ChildAuditLogger(workspace_dir)
|
||
_cs_audit.cleanup_old_logs(_cs_config.audit_retention_days)
|
||
_cs_filter = ChildSafetyFilter(_cs_config, _cs_audit)
|
||
|
||
self.add_preprocessor(_cs_filter.preprocess_adapter)
|
||
self.add_postprocessor(_cs_filter.postprocess_adapter)
|
||
```
|
||
|
||
The `preprocess_adapter` and `postprocess_adapter` wrappers adapt the filter's return types
|
||
to match the runtime's expected preprocessor/postprocessor signatures.
|
||
|
||
For blocking: the preprocessor adapter queues the safe reply via the adapter's `send_message()`
|
||
and returns a sentinel `InboundMessage` that causes the agent to be skipped. Review the runtime
|
||
queue loop in `_process_message()` to confirm the cleanest abort point.
|
||
|
||
**Acceptance:** End-to-end test with gabriel's Slack account (or mocked username):
|
||
- Allowed message → full response delivered, audit entry written
|
||
- Blocked message → canned reply delivered immediately, no LLM call made
|
||
|
||
---
|
||
|
||
## Phase 6 — Verification
|
||
|
||
### TASK-09 — End-to-end smoke test
|
||
**What:** Manual testing checklist before considering the feature complete.
|
||
|
||
- [ ] Gabriel's Slack ID is mapped and he can message the bot
|
||
- [ ] Allowed game dev question gets a helpful Lua/Roblox response
|
||
- [ ] Blocked real-world harm question gets canned reply, no LLM call
|
||
- [ ] Horror game question with violence words (e.g., "enemy takes damage") passes through
|
||
- [ ] Audit log file created at correct path with correct schema
|
||
- [ ] Parent (Jordan/Cloe) messages are completely unaffected — full SOUL.md + context.md injected as normal
|
||
- [ ] RSO interaction log unchanged (no extra entries for blocked messages)
|
||
- [ ] Bot restart preserves config (no in-memory-only state)
|
||
- [ ] Gabriel's system prompt does NOT contain SOUL.md or context.md content (verify via debug)
|
||
- [ ] Jordan's system prompt still contains SOUL.md and context.md (verify no regression)
|
||
|
||
### TASK-10 — Review and finalise Gabriel's user profile
|
||
**File:** `memory_workspace/users/gabriel.md`
|
||
**What:** After first real interactions, update the profile with observed interests, current
|
||
project details, preferred explanation style. This is an ongoing task.
|
||
**Depends on:** TASK-09
|
||
|
||
---
|
||
|
||
## Phase 7 — Token Optimization
|
||
|
||
### TASK-11 — Implement stripped system prompt for restricted users
|
||
**File:** `agent.py`, `_build_system_prompt()` and `_chat_inner()`
|
||
**What:** Build the child-optimized system prompt path; reduce history window for restricted users.
|
||
**Depends on:** TASK-07 (child safety config already wired into Agent)
|
||
|
||
Changes to `_build_system_prompt()`:
|
||
1. Add module-level constant `CHILD_TUTOR_IDENTITY` — ~100-token identity replacing SOUL.md
|
||
2. Add module-level constant `CHILD_MAX_CONTEXT_MESSAGES = 10`
|
||
3. When `is_restricted(username)` is true, build a stripped prompt:
|
||
|
||
```python
|
||
def _build_child_system_prompt(self, username: str) -> str:
|
||
user_profile = self.memory.get_user(username)
|
||
parts = [
|
||
CHILD_TUTOR_IDENTITY,
|
||
f"User Profile:\n{user_profile}",
|
||
CHILD_GUARDRAIL_BLOCK,
|
||
"You have access to tools for web search and code help. "
|
||
"Use them to assist Gabriel with his game development questions.",
|
||
]
|
||
return "\n\n".join(parts)
|
||
```
|
||
|
||
Note: `get_soul()`, `get_context()`, `search_hybrid()`, and the delegation block are
|
||
all deliberately absent.
|
||
|
||
Changes to `_chat_inner()`:
|
||
```python
|
||
is_child = (self._child_safety_config
|
||
and self._child_safety_config.is_restricted(username))
|
||
cap = CHILD_MAX_CONTEXT_MESSAGES if is_child else MAX_CONTEXT_MESSAGES
|
||
system = (self._build_child_system_prompt(username) if is_child
|
||
else self._build_system_prompt(user_message, username, platform))
|
||
```
|
||
|
||
**Acceptance:**
|
||
- Gabriel's assembled system prompt: no SOUL.md text, no SSH/Proxmox content, no delegation block
|
||
- Gabriel's history window: max 10 messages passed to LLM
|
||
- Jordan's assembled system prompt: unchanged (full SOUL.md + context.md present)
|
||
- Measure approximate token count difference (log `len(system)` for both users)
|
||
|
||
### TASK-12 — Add AI literacy guidance to gabriel.md
|
||
**File:** `memory_workspace/users/gabriel.md`
|
||
**What:** Add a section explicitly coaching the model on how to teach Gabriel to use the bot
|
||
well — question framing, context windows, spotting assumptions. This lives in the profile (not
|
||
the guardrail block) so it can be updated over time as his skills grow.
|
||
**Depends on:** TASK-02 (profile already exists)
|
||
|
||
Add a section to gabriel.md:
|
||
|
||
```markdown
|
||
## Teaching Him to Use AI Well
|
||
|
||
Help Gabriel get better at using AI tools as a skill in itself. Do this naturally, not as
|
||
a lesson — just model good practice and name it when it happens.
|
||
|
||
- When he asks something vague: clarify first, then answer. Name what you're doing.
|
||
"Just checking what you mean first — that's a good habit when asking any AI."
|
||
- When context runs out or he notices you "forgot": explain the context window simply.
|
||
"I can only hold so much conversation at once — like a whiteboard that fills up."
|
||
- When he nails a well-structured question: tell him.
|
||
"That was a perfect question — gave me exactly what I needed."
|
||
- Teach the format: what I have / what I want / what I've tried.
|
||
- Flag your own assumptions visibly so he learns to spot ambiguity in questions.
|
||
```
|
||
|
||
**Acceptance:** Profile updated; the AI literacy guidance appears in Gabriel's system prompt
|
||
on the next session.
|
||
|
||
---
|
||
|
||
## Summary Table
|
||
|
||
| Task | File | Phase | Depends On |
|
||
|------|------|-------|-----------|
|
||
| TASK-01 | `config/adapters.local.yaml` | 1 — Identity | Parent provides Slack ID |
|
||
| TASK-02 | `memory_workspace/users/gabriel.md` | 1 — Identity | — |
|
||
| TASK-03 | `child_safety.py` | 2 — Audit | — |
|
||
| TASK-04 | `child_safety.py` | 3 — Filter | TASK-01 |
|
||
| TASK-05 | `child_safety.py` | 3 — Filter | TASK-03, TASK-04 |
|
||
| TASK-06 | `child_safety.py` | 3 — Filter | TASK-05 |
|
||
| TASK-07 | `agent.py` | 4 — System Prompt | TASK-04 |
|
||
| TASK-08 | `adapters/runtime.py` | 5 — Wiring | TASK-05, TASK-06 |
|
||
| TASK-09 | — | 6 — Verify | All |
|
||
| TASK-10 | `memory_workspace/users/gabriel.md` | 6 — Verify | TASK-09 |
|
||
| TASK-11 | `agent.py` | 7 — Token Optimization | TASK-07 |
|
||
| TASK-12 | `memory_workspace/users/gabriel.md` | 7 — Token Optimization | TASK-02 |
|
||
| TASK-13 | `adapters/slack/adapter.py` | 8 — Slack Allow-List | — |
|
||
| TASK-14 | `agent.py`, `gabriel_context.md` | 9 — Continuity | TASK-11 |
|
||
| TASK-15 | `agent.py` | 9 — Continuity | TASK-14 |
|
||
| TASK-16 | — | 10 — Full Verify | All |
|
||
|
||
---
|
||
|
||
## Phase 8 — Slack Allow-List
|
||
|
||
### TASK-13 — Add allow-list check to Slack adapter
|
||
**File:** `adapters/slack/adapter.py`
|
||
**What:** Silently drop messages from users not in `allowed_users` config.
|
||
**Depends on:** Nothing (standalone fix)
|
||
|
||
Add to `SlackAdapter`:
|
||
|
||
```python
|
||
def _is_user_allowed(self, user_id: str) -> bool:
|
||
allowed = self.config.settings.get("allowed_users", [])
|
||
if not allowed:
|
||
return True # open if unconfigured — preserves existing behaviour
|
||
return str(user_id) in [str(u) for u in allowed]
|
||
```
|
||
|
||
In `handle_message_events()`, first line after extracting `user_id`:
|
||
```python
|
||
if not self._is_user_allowed(user_id):
|
||
return
|
||
```
|
||
|
||
Update `config/adapters.local.yaml` to add `allowed_users` under the slack block:
|
||
```yaml
|
||
slack:
|
||
allowed_users:
|
||
- U01234JORDAN # Jordan's Slack user ID (find via Slack profile → More → Copy member ID)
|
||
- U09876GABRIEL # Gabriel's Slack user ID
|
||
```
|
||
|
||
**Acceptance:** A Slack message from a user not in the list produces no bot response.
|
||
Jordan and Gabriel's messages continue to work normally.
|
||
|
||
---
|
||
|
||
## Phase 9 — Cross-Session Continuity & First-Run
|
||
|
||
### TASK-14 — Implement gabriel_context.md injection and end-of-session update
|
||
**File:** `agent.py`, `_build_child_system_prompt()`
|
||
**What:** Inject `gabriel_context.md` into Gabriel's system prompt; instruct the bot to
|
||
update it at the end of each session.
|
||
**Depends on:** TASK-11 (child system prompt path)
|
||
|
||
Changes to `_build_child_system_prompt()`:
|
||
|
||
```python
|
||
context_path = self.memory.workspace_dir / "users" / "gabriel_context.md"
|
||
gabriel_context = context_path.read_text(encoding="utf-8") if context_path.exists() else None
|
||
is_first_run = gabriel_context is None
|
||
|
||
parts = [CHILD_TUTOR_IDENTITY, f"User Profile:\n{user_profile}"]
|
||
if gabriel_context:
|
||
parts.append(f"Project Context & Skills:\n{gabriel_context}")
|
||
if is_first_run:
|
||
parts.append(FIRST_RUN_BLOCK) # see TASK-15
|
||
parts.append(CHILD_GUARDRAIL_BLOCK)
|
||
parts.append(SESSION_UPDATE_INSTRUCTION) # always appended
|
||
parts.append("You have access to file tools. Use them to update gabriel_context.md "
|
||
"at the end of this conversation.")
|
||
```
|
||
|
||
`SESSION_UPDATE_INSTRUCTION` constant:
|
||
```
|
||
At the end of this conversation, use your file write tool to update
|
||
`memory_workspace/users/gabriel_context.md` with:
|
||
- ## Active Project: what Gabriel is building (name + one sentence description)
|
||
- ## Last Session (today's date): what was worked on, bugs fixed, concepts covered
|
||
- ## Open Threads: anything Gabriel mentioned wanting to do next
|
||
- ## Skills Introduced: cumulative list of concepts taught, with date first introduced
|
||
|
||
Keep the file under 40 lines. Overwrite it completely each time.
|
||
```
|
||
|
||
**Acceptance:**
|
||
- Second session: Gabriel's project from previous session appears in system prompt
|
||
- File is updated after the session ends (check `gabriel_context.md` was written)
|
||
- First session: file absent, bot starts fresh, file created after first exchange
|
||
|
||
### TASK-15 — Implement first-run welcome
|
||
**File:** `agent.py`, `child_safety.py`
|
||
**What:** Send a warm onboarding welcome on Gabriel's very first message.
|
||
**Depends on:** TASK-14 (first-run detection)
|
||
|
||
`FIRST_RUN_BLOCK` constant (added to `child_safety.py`):
|
||
```
|
||
FIRST SESSION: This is Gabriel's very first message. Before answering his question,
|
||
send a short friendly welcome (4–5 sentences max). Cover:
|
||
- What you can help with: Lua, Roblox Studio, game design, coding questions
|
||
- That you guide and teach rather than just hand over answers
|
||
- That you'll remember his projects between sessions
|
||
- Invite him to tell you what he's building (or answer if he already has)
|
||
Casual and warm — not a formal introduction. Then answer his question normally.
|
||
```
|
||
|
||
This block is appended to `_build_child_system_prompt()` only when `is_first_run is True`.
|
||
|
||
**Acceptance:**
|
||
- First ever message from Gabriel → bot sends welcome then answers the question
|
||
- Second session → no welcome, goes straight to the question
|
||
- Jordan's sessions → no welcome block ever injected
|