Privacy mode left the AI blind twice over: the whole prompt was a token, so it could not help with light, lens or style, and model and LoRA file names still went out as written. - The prompt box has a first field, the gist: the picture in words the user writes for the AI to read. seal_answer hands it on as written beside the tokens of the private prompts; either alone can be handed in. - ai_sealed_models.py: while the Gates window's "Model names in a private task" is on (the default), each model of the library is one token ([M1], sealed as its model_id) however an answer writes it: model_id, loader name, file name in a text, card name. A model's own entry drops name, file, folder, links and hash and keeps type, base model, the author's settings and the user's note. expand writes a model token back as the model_id, as the loader's name among a node's values, as the card's name inside the AI's words. The task's menu lists what each token stands for. - The model editor has "Note for the AI" (ai_note, note_for_ai in the tools, searched by search_models): what a model is for, in words the AI may read. - A combo's name is sealed too: unless renamed it is made of its model's name and the start of its prompt. - The privacy skill says how to write around a gist and how to pick and pass on model tokens. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
122 lines
5.3 KiB
Python
122 lines
5.3 KiB
Python
"""Privacy mode (隐私模式): a task the user keeps private sends the AI none of its pictures and no prompt
|
|
text. The AI still builds and runs workflows and knows the models; it hears how many pictures came out
|
|
and whether the run went well, and the user looks at them in Anomalous. Prompts and picture names
|
|
reach it as tokens it passes on (ai_sealed.py); the user writes prompts in a prompt box. Each task has its switch (task["privacy"],
|
|
in the task's header); new tasks follow the default the user sets in the Gates window
|
|
(user/anomalous/ai_privacy.json, {default, models}). Turning it on or off is a step the AI is told of, and one of the
|
|
task's decisions (ai_decisions.py). A private task keeps its models' names too, as tokens the AI picks by type,
|
|
base model and the user's note (ai_sealed_models.py), unless the user lets the names through in the Gates window.
|
|
|
|
All of it happens at one place: mcp_server.py puts the texts back into a call's arguments
|
|
(ai_sealed.expand) and hands every tool result to guard() before it leaves, whatever tool or later
|
|
step (run pictures, the user's messages) put a picture or a prompt in it. A call that
|
|
cannot be tied to a task is private while the default is on or any private task is unfinished:
|
|
when in doubt, nothing is shown. The card in the AI app (mcp_apps.py) shows no pictures either.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
from . import ai_sealed, ai_tasks
|
|
from .path_utils import atomic_write_json
|
|
|
|
NOTE = ("Privacy mode: no pictures or prompt text reach you, only tokens like [P1], [img1] and [M1] (a model) to pass "
|
|
"on as they are. get_toolbox(skill=\"privacy\") first.")
|
|
|
|
|
|
def _path():
|
|
return os.path.join(os.path.dirname(ai_tasks.tasks_dir()), "ai_privacy.json")
|
|
|
|
|
|
def _kept():
|
|
try:
|
|
with open(_path(), encoding="utf-8") as source:
|
|
data = json.load(source)
|
|
except (OSError, ValueError):
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def default_on():
|
|
"""Whether new tasks are private (the Gates window); off unless the user turned it on."""
|
|
return _kept().get("default") is True
|
|
|
|
|
|
def hides_models():
|
|
"""Whether a private task keeps its models' names too (the Gates window); it does unless the user lets them through."""
|
|
return _kept().get("models") is not False
|
|
|
|
|
|
def set_hides_models(on):
|
|
if not isinstance(on, bool):
|
|
raise ValueError("privacy_models is true or false")
|
|
with ai_tasks._lock:
|
|
atomic_write_json(_path(), {**_kept(), "models": on})
|
|
return on
|
|
|
|
|
|
def set_default(on):
|
|
"""The default for new tasks. Turned off, the tasks that were private by it stay private."""
|
|
if not isinstance(on, bool):
|
|
raise ValueError("privacy is true or false")
|
|
with ai_tasks._lock:
|
|
if not on and default_on():
|
|
for task in ai_tasks._load().values():
|
|
if "privacy" not in task:
|
|
task["privacy"] = True
|
|
ai_tasks._save(task)
|
|
atomic_write_json(_path(), {**_kept(), "default": on})
|
|
return on
|
|
|
|
|
|
def is_private(task):
|
|
"""The task's own choice, else the default."""
|
|
value = (task or {}).get("privacy")
|
|
return value if isinstance(value, bool) else default_on()
|
|
|
|
|
|
def set_private(task_id, on):
|
|
"""The user's choice for one task, told to the AI as a step of theirs. Returns it."""
|
|
if not isinstance(on, bool):
|
|
raise ValueError("privacy is true or false")
|
|
with ai_tasks._lock:
|
|
task = ai_tasks._load().get(task_id)
|
|
if not task:
|
|
raise ValueError("No such task")
|
|
if is_private(task) != on:
|
|
ai_tasks._add_step(task, {"id": uuid.uuid4().hex[:10], "at": time.time(), "ms": 0, "tool": "user_privacy", "kind": "user",
|
|
"ok": True, "state": "done", "summary": "user_privacy", "privacy": on, "delivered": False, "results": [],
|
|
"text": "The user turned privacy mode on for this task. " + NOTE if on else
|
|
"The user turned privacy mode off for this task: you get the pictures again."})
|
|
task["privacy"] = on
|
|
ai_tasks._save(task)
|
|
ai_tasks._notify(task_id)
|
|
return on
|
|
|
|
|
|
def applies(task_id):
|
|
"""Whether a call of task `task_id` (None: none known) is private."""
|
|
task = ai_tasks.get_task(task_id) if task_id else None
|
|
if task:
|
|
return is_private(task)
|
|
if default_on():
|
|
return True
|
|
with ai_tasks._lock:
|
|
return any(item.get("privacy") is True and not item.get("ended") for item in ai_tasks._load().values())
|
|
|
|
|
|
def guard(result, private, task_id=None, info=None, tool=""):
|
|
"""`result` as it may leave: a private call's pictures taken out (with a word on why), its prompt
|
|
text, picture names and (hides_models) models as tokens (ai_sealed.mask; `info`: ComfyUI's node list)."""
|
|
if not private or not isinstance(result, dict):
|
|
return result
|
|
content = [item for item in result.get("content") or [] if not (isinstance(item, dict) and item.get("type") == "image")]
|
|
if len(content) != len(result.get("content") or []):
|
|
content.append({"type": "text", "text": NOTE})
|
|
result["content"] = content
|
|
if isinstance(result.get("structuredContent"), dict):
|
|
result["structuredContent"] = {**result["structuredContent"], "privacy": NOTE}
|
|
return ai_sealed.mask(result, task_id, info, tool, hides_models())
|