ai_sealed keeps a private task's prompt text and picture file names behind tokens ([P1], [img1], task["sealed"]): mcp_server puts the texts back into a call's arguments for the tool only, and ai_privacy.guard has every result masked on its way out (a node's STRING inputs by ComfyUI's node list, prompt fields, picture names, any sealed text, also in the text content). The user's own canvas changes are told sealed. A "prompts" panel block is the user's prompt box (ui_ai_prompt_box.js): prompts, a negative one and reference pictures from the gallery or dropped in, answered only from Anomalous, the AI getting tokens; above it the prompt_pose skill's words for the canvas's model. A new private-only skill, privacy, tells the AI how to work, and begin_task points a private task at it. Tests plant canary prompts and picture names in every tool's result and check none gets out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.6 KiB
Python
69 lines
3.6 KiB
Python
"""The user's own changes in Anomalous while an AI works on a task: each activity log entry the
|
|
user makes (activity_log.py tells note_activity of every new entry) becomes a step of the task
|
|
the AI called within USER_STEP_SECONDS, and the entry names that task back. The AI is told of it
|
|
with its next call (ai_panels.attach_pending, a message of kind "change"), so it keeps the user's
|
|
change instead of working over it. The AI's own canvas changes (via mcp) are its steps already;
|
|
which actions count, and their stage, is action_catalog.USER_ACTIONS. A private task's
|
|
(ai_privacy.py) are told with what was typed sealed (ai_sealed.py): the AI hears that a prompt
|
|
changed, not what it says."""
|
|
|
|
import time
|
|
import uuid
|
|
|
|
from . import action_catalog, activity_log, ai_privacy, ai_sealed, ai_tasks
|
|
|
|
USER_STEP_SECONDS = 10 * 60
|
|
|
|
|
|
def _live_task(now):
|
|
"""The task an AI is working on now (it called within USER_STEP_SECONDS), or None."""
|
|
live = [task for task in ai_tasks._load().values() if not task.get("ended") and not task.get("waiting")
|
|
and now - task.get("updated", 0) < USER_STEP_SECONDS]
|
|
return max(live, key=lambda task: task.get("updated", 0)) if live else None
|
|
|
|
|
|
def _change_text(entry, seal=lambda value: value):
|
|
"""What the user changed, in words for the AI; `seal(value)` for what was before and after."""
|
|
if entry.get("action") == "image_delete" and entry.get("target"):
|
|
return (f"Deleted the picture {entry['target']} (to the Recycle Bin): it is gone from the output folder, "
|
|
"so do not use, show or hand it in again; another picture is needed for what it was for.")
|
|
if entry.get("source") != "canvas":
|
|
words = str(entry.get("action", "")).replace("_", " ")
|
|
return f"{words}: {entry['target']}" if entry.get("target") else words
|
|
lines = []
|
|
for change in (entry.get("detail") or {}).get("changes") or []:
|
|
if change.get("kind") == "changed":
|
|
lines.append(f"{change.get('node')} {change.get('widget')}: {seal(change.get('before'))} -> {seal(change.get('after'))}")
|
|
elif change.get("kind") == "opened":
|
|
lines.append(f"opened the workflow {change.get('node')}")
|
|
else:
|
|
lines.append(f"{change.get('kind')} {change.get('node')}")
|
|
return "Changed on the canvas: " + "; ".join(lines[:8])
|
|
|
|
|
|
def note_activity(entry):
|
|
"""A new activity log entry: a step of the AI's task when the user made it while the AI worked."""
|
|
stage = action_catalog.USER_ACTIONS.get(entry.get("action"))
|
|
if stage is None or (entry.get("detail") or {}).get("via") == "mcp":
|
|
return None
|
|
at = entry.get("time") or time.time()
|
|
with ai_tasks._lock:
|
|
task = _live_task(at)
|
|
if not task:
|
|
return None
|
|
seals = ai_sealed.Seals(task["id"]) if ai_privacy.is_private(task) else None
|
|
text = _change_text(entry, (lambda value: ai_sealed.seal_text(seals, value)) if seals else (lambda value: value))
|
|
if seals:
|
|
seals.save()
|
|
step = {"id": uuid.uuid4().hex[:10], "at": at, "ms": 0, "tool": "user_activity", "kind": "user", "stage": stage,
|
|
"ok": True, "state": "done", "summary": entry.get("action", ""),
|
|
"text": ai_tasks._short(text, ai_tasks.MAX_NOTE), "activity": entry["id"],
|
|
"delivered": False, "results": []}
|
|
# Appended without moving "updated": the user's own changes do not keep the AI's task open.
|
|
task["steps"].append(step)
|
|
del task["steps"][:-ai_tasks.MAX_STEPS]
|
|
ai_tasks._save(task)
|
|
activity_log.tag(entry["id"], task["id"])
|
|
ai_tasks._notify(task["id"])
|
|
return step
|