Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-20) — Environment-Scoped Typed Asset Store](#whats-new-2026-06-20--environment-scoped-typed-asset-store)
- [What's new (2026-06-20) — Task / Process Mining (Automation-Candidate Discovery)](#whats-new-2026-06-20--task--process-mining-automation-candidate-discovery)
- [What's new (2026-06-20) — Stuck-Loop Guard (Agent Loop Progress Detection)](#whats-new-2026-06-20--stuck-loop-guard-agent-loop-progress-detection)
- [What's new (2026-06-20) — Coordinate-Space Mapping (Model Grid ⇄ Physical Pixels)](#whats-new-2026-06-20--coordinate-space-mapping-model-grid--physical-pixels)
Expand Down Expand Up @@ -100,6 +101,12 @@

---

## What's new (2026-06-20) — Environment-Scoped Typed Asset Store

Per-environment typed config + credential refs. Full reference: [`docs/source/Eng/doc/new_features/v48_features_doc.rst`](docs/source/Eng/doc/new_features/v48_features_doc.rst).

- **`AssetStore` / `active_environment`** (`AC_set_asset` / `AC_get_asset` / `AC_list_assets`, `ac_*`): the orchestrator "Assets/lockers" pillar — centrally-managed config values that differ by environment (dev/staging/prod) and carry a type (`text`/`int`/`bool`/`credential`). `get` coerces to the declared type and falls back to the default env; `credential` assets hold a secret *reference* that `resolve` turns into the real value via an injected resolver (Python-only, so secrets never enter `get`/executor records). Fills the gap the secret vault (secret-only) and config-sync (whole-blob) left.

## What's new (2026-06-20) — Task / Process Mining (Automation-Candidate Discovery)

Discover what to automate from recorded action logs. Full reference: [`docs/source/Eng/doc/new_features/v47_features_doc.rst`](docs/source/Eng/doc/new_features/v47_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-20) — 环境范围的带类型资产存储](#本次更新-2026-06-20--环境范围的带类型资产存储)
- [本次更新 (2026-06-20) — 任务 / 流程挖掘(自动化候选发现)](#本次更新-2026-06-20--任务--流程挖掘自动化候选发现)
- [本次更新 (2026-06-20) — 卡循环守卫(Agent Loop 进度检测)](#本次更新-2026-06-20--卡循环守卫agent-loop-进度检测)
- [本次更新 (2026-06-20) — 坐标空间映射(模型网格 ⇄ 物理像素)](#本次更新-2026-06-20--坐标空间映射模型网格--物理像素)
Expand Down Expand Up @@ -99,6 +100,12 @@

---

## 本次更新 (2026-06-20) — 环境范围的带类型资产存储

依环境的带类型配置 + credential 引用。完整参考:[`docs/source/Zh/doc/new_features/v48_features_doc.rst`](../docs/source/Zh/doc/new_features/v48_features_doc.rst)。

- **`AssetStore` / `active_environment`**(`AC_set_asset` / `AC_get_asset` / `AC_list_assets`、`ac_*`):orchestrator 的「Assets/lockers」支柱 —— 集中管理、依环境(dev/staging/prod)而异且带类型(`text`/`int`/`bool`/`credential`)的配置值。`get` 转成声明类型并退回 default 环境;`credential` 资产持有密钥*引用*,由 `resolve` 通过注入解析器转成真实值(仅限 Python,因此密钥永不进入 `get`/executor 记录)。补足密钥保险库(仅密钥)与 config-sync(整块)的缺口。

## 本次更新 (2026-06-20) — 任务 / 流程挖掘(自动化候选发现)

从录制的动作日志发现该自动化什么。完整参考:[`docs/source/Zh/doc/new_features/v47_features_doc.rst`](../docs/source/Zh/doc/new_features/v47_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-20) — 環境範圍的具型別資產儲存](#本次更新-2026-06-20--環境範圍的具型別資產儲存)
- [本次更新 (2026-06-20) — 任務 / 流程探勘(自動化候選發現)](#本次更新-2026-06-20--任務--流程探勘自動化候選發現)
- [本次更新 (2026-06-20) — 卡迴圈守衛(Agent Loop 進度偵測)](#本次更新-2026-06-20--卡迴圈守衛agent-loop-進度偵測)
- [本次更新 (2026-06-20) — 座標空間對映(模型網格 ⇄ 實體像素)](#本次更新-2026-06-20--座標空間對映模型網格--實體像素)
Expand Down Expand Up @@ -99,6 +100,12 @@

---

## 本次更新 (2026-06-20) — 環境範圍的具型別資產儲存

依環境的具型別設定 + credential 參照。完整參考:[`docs/source/Zh/doc/new_features/v48_features_doc.rst`](../docs/source/Zh/doc/new_features/v48_features_doc.rst)。

- **`AssetStore` / `active_environment`**(`AC_set_asset` / `AC_get_asset` / `AC_list_assets`、`ac_*`):orchestrator 的「Assets/lockers」支柱 —— 集中管理、依環境(dev/staging/prod)而異且帶型別(`text`/`int`/`bool`/`credential`)的設定值。`get` 轉成宣告型別並退回 default 環境;`credential` 資產持有密鑰*參照*,由 `resolve` 透過注入解析器轉成真實值(僅限 Python,因此密鑰永不進入 `get`/executor 紀錄)。補足密鑰保險庫(僅密鑰)與 config-sync(整塊)的缺口。

## 本次更新 (2026-06-20) — 任務 / 流程探勘(自動化候選發現)

從錄製的動作日誌發現該自動化什麼。完整參考:[`docs/source/Zh/doc/new_features/v47_features_doc.rst`](../docs/source/Zh/doc/new_features/v47_features_doc.rst)。
Expand Down
54 changes: 54 additions & 0 deletions docs/source/Eng/doc/new_features/v48_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Environment-Scoped Typed Asset Store
====================================

Flows need centrally-managed config values that differ by environment
(dev/staging/prod) and carry a type — the orchestrator "Assets / lockers"
pillar. The secret vault covers secrets only and config-sync moves whole blobs;
neither offers a typed, per-environment named lookup. ``AssetStore`` fills that:
values are stored under an environment, read back with type coercion, and
``credential`` assets hold a *reference* (a secret name) that :meth:`resolve`
turns into the real value through an injected resolver — so the secret never
lands in a plain ``get`` or an executor record.

JSON-backed (or in-memory); pure standard library; imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import AssetStore, active_environment

store = AssetStore("assets.json")
store.set("max_retries", 3, asset_type="int", environment="prod")
store.set("api_base", "https://prod.example.com", environment="prod")
store.set("db_password", "vault_db_pw", asset_type="credential") # value = a ref

store.get("max_retries", environment="prod").value # -> 3 (typed)
store.get("api_base", environment="staging").value # -> falls back to default
store.get("db_password").value # -> "vault_db_pw" (reference, safe)

# resolve a credential through an injected secret resolver (Python-only):
store = AssetStore("assets.json", secret_resolver=secret_manager.get)
store.resolve("db_password") # -> the real secret

Types are ``text`` / ``int`` / ``bool`` / ``credential``; ``get`` coerces to the
declared type and falls back to the ``default`` environment unless disabled.
``active_environment()`` reads ``JE_AUTOCONTROL_ENV``. ``list`` / ``delete`` round
out the store.

Executor commands
-----------------

================================ ===================================================
Command Effect
================================ ===================================================
``AC_set_asset`` Store a typed, environment-scoped asset.
``AC_get_asset`` Read an asset (credential stays a reference).
``AC_list_assets`` List ``{name, type, environment}`` (no values).
================================ ===================================================

Credential **resolution** is intentionally Python-API-only (so secrets never
enter run records). The same lifecycle operations are exposed as MCP tools
(``ac_set_asset`` / ``ac_get_asset`` / ``ac_list_assets``) and as Script Builder
commands under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v45_features_doc
doc/new_features/v46_features_doc
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
49 changes: 49 additions & 0 deletions docs/source/Zh/doc/new_features/v48_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
環境範圍的具型別資產儲存
========================

流程需要集中管理、依環境(dev/staging/prod)而異且帶有型別的設定值 —— 這是 orchestrator
的「Assets / lockers」支柱。密鑰保險庫只處理密鑰,config-sync 搬移整塊設定;兩者皆無具
型別、依環境的具名查詢。``AssetStore`` 補足此處:值依環境儲存、讀回時做型別轉換,而
``credential`` 資產持有一個*參照*(密鑰名稱),由 :meth:`resolve` 透過注入的解析器轉成
真實值 —— 因此密鑰永不出現在純 ``get`` 或 executor 紀錄中。

JSON 後端(或記憶體內);純標準函式庫;不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import AssetStore, active_environment

store = AssetStore("assets.json")
store.set("max_retries", 3, asset_type="int", environment="prod")
store.set("api_base", "https://prod.example.com", environment="prod")
store.set("db_password", "vault_db_pw", asset_type="credential") # value = 參照

store.get("max_retries", environment="prod").value # -> 3(具型別)
store.get("api_base", environment="staging").value # -> 退回 default
store.get("db_password").value # -> "vault_db_pw"(參照,安全)

# 透過注入的密鑰解析器解析 credential(僅限 Python):
store = AssetStore("assets.json", secret_resolver=secret_manager.get)
store.resolve("db_password") # -> 真實密鑰

型別為 ``text`` / ``int`` / ``bool`` / ``credential``;``get`` 會轉成宣告型別,並在未停用
時退回 ``default`` 環境。``active_environment()`` 讀取 ``JE_AUTOCONTROL_ENV``。``list`` /
``delete`` 補齊整個儲存體。

執行器指令
----------

================================ ===================================================
指令 效果
================================ ===================================================
``AC_set_asset`` 儲存具型別、依環境的資產。
``AC_get_asset`` 讀取資產(credential 維持參照)。
``AC_list_assets`` 列出 ``{name, type, environment}``(不含值)。
================================ ===================================================

credential 的**解析**刻意僅限 Python API(因此密鑰永不進入執行紀錄)。相同的生命週期操
作亦提供為 MCP 工具(``ac_set_asset`` / ``ac_get_asset`` / ``ac_list_assets``),以及
Script Builder 中 **Data** 分類下的指令。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v45_features_doc
doc/new_features/v46_features_doc
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@
Candidate, MiningReport, SequencePattern, directly_follows,
find_repeated_sequences, mine_action_log, rank_automation_candidates,
)
# Environment-scoped typed asset/config store
from je_auto_control.utils.assets import (
Asset, AssetStore, AssetValue, active_environment,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -723,6 +727,7 @@ def start_autocontrol_gui(*args, **kwargs):
"Candidate", "MiningReport", "SequencePattern", "directly_follows",
"find_repeated_sequences", "mine_action_log",
"rank_automation_candidates",
"Asset", "AssetStore", "AssetValue", "active_environment",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
32 changes: 32 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,38 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Find repeated sequences + rank automation candidates.",
))
specs.append(CommandSpec(
"AC_set_asset", "Data", "Asset: Set",
fields=(
FieldSpec("name", FieldType.STRING),
FieldSpec("value", FieldType.STRING),
FieldSpec("asset_type", FieldType.ENUM, optional=True,
default="text",
choices=("text", "int", "bool", "credential")),
FieldSpec("environment", FieldType.STRING, optional=True,
default="default"),
FieldSpec("db", FieldType.STRING, optional=True),
),
description="Store a typed, environment-scoped asset.",
))
specs.append(CommandSpec(
"AC_get_asset", "Data", "Asset: Get",
fields=(
FieldSpec("name", FieldType.STRING),
FieldSpec("environment", FieldType.STRING, optional=True,
default="default"),
FieldSpec("db", FieldType.STRING, optional=True),
),
description="Read a typed asset (credential stays a reference).",
))
specs.append(CommandSpec(
"AC_list_assets", "Data", "Asset: List",
fields=(
FieldSpec("environment", FieldType.STRING, optional=True),
FieldSpec("db", FieldType.STRING, optional=True),
),
description="List assets (name/type/environment, no values).",
))
specs.append(CommandSpec(
"AC_generate_sop", "Report", "Generate SOP Document",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/assets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Environment-scoped typed asset/config store (UiPath-Assets style)."""
from je_auto_control.utils.assets.assets import (
Asset, AssetStore, AssetValue, active_environment,
)

__all__ = ["Asset", "AssetStore", "AssetValue", "active_environment"]
149 changes: 149 additions & 0 deletions je_auto_control/utils/assets/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Typed, environment-scoped assets — the orchestrator "Assets / lockers" pillar.

Flows need centrally-managed config values that differ by environment
(dev/staging/prod) and carry a type (text/int/bool/credential). The secret vault
covers secrets only and config-sync moves whole blobs; neither offers a typed,
per-environment named lookup. ``AssetStore`` fills that: values are stored under
an environment, read back with type coercion, and ``credential`` assets hold a
*reference* (a secret name) that :meth:`AssetStore.resolve` turns into the real
value through an injected resolver — so the secret never lands in a plain
``get``/executor record.

JSON-backed (or in-memory); pure standard library; imports no ``PySide6``.
"""
import os
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional

from je_auto_control.utils.json_store import read_json_dict, write_json_dict

ENV_VAR = "JE_AUTOCONTROL_ENV"
DEFAULT_ENV = "default"
TYPE_TEXT = "text"
TYPE_INT = "int"
TYPE_BOOL = "bool"
TYPE_CREDENTIAL = "credential"


@dataclass(frozen=True)
class Asset:
"""A stored asset record (``value`` is a secret *name* when credential)."""

name: str
type: str
environment: str
value: Any


@dataclass(frozen=True)
class AssetValue:
"""A read asset with its declared type and coerced (non-secret) value."""

name: str
type: str
value: Any


def active_environment() -> str:
"""Return the active environment from ``JE_AUTOCONTROL_ENV`` (or default)."""
return os.environ.get(ENV_VAR, DEFAULT_ENV)


def _coerce(value: Any, type_name: str) -> Any:
if type_name == TYPE_INT:
return int(value)
if type_name == TYPE_BOOL:
if isinstance(value, str):
return value.strip().lower() in ("1", "true", "yes", "on")
return bool(value)
return value if type_name == TYPE_CREDENTIAL else str(value)


class AssetStore:
"""A typed, environment-scoped key/value store backed by optional JSON."""

def __init__(self, db_path: Optional[str] = None, *,
secret_resolver: Optional[Callable[[str], Any]] = None
) -> None:
"""``secret_resolver(name)`` resolves ``credential`` references lazily."""
self._path = db_path
self._resolver = secret_resolver
self._data: Dict[str, Dict[str, Dict[str, Any]]] = read_json_dict(
db_path)

def _flush(self) -> None:
if self._path is not None:
write_json_dict(self._path, self._data)

def set(self, name: str, value: Any, *, asset_type: str = TYPE_TEXT,
environment: str = DEFAULT_ENV) -> None:
"""Store ``value`` for ``name`` under ``environment`` with a type tag."""
self._data.setdefault(environment, {})[name] = {
"type": asset_type, "value": value}
self._flush()

def _lookup(self, name: str, environment: str,
fallback_to_default: bool) -> Optional[Dict[str, Any]]:
record = self._data.get(environment, {}).get(name)
if record is None and fallback_to_default and environment != DEFAULT_ENV:
record = self._data.get(DEFAULT_ENV, {}).get(name)
return record

def get(self, name: str, *, environment: str = DEFAULT_ENV,
fallback_to_default: bool = True) -> AssetValue:
"""Return the typed asset (credential values stay as a reference)."""
record = self._lookup(name, environment, fallback_to_default)
if record is None:
raise KeyError(f"asset {name!r} not found for {environment!r}")
type_name = str(record["type"])
return AssetValue(name, type_name,
_coerce(record["value"], type_name))

def resolve(self, name: str, *, environment: str = DEFAULT_ENV) -> Any:
"""Like :meth:`get` but resolves a ``credential`` to its real value."""
asset = self.get(name, environment=environment)
if asset.type != TYPE_CREDENTIAL:
return asset.value
if self._resolver is None:
raise RuntimeError("no secret_resolver configured for credentials")
return self._resolver(str(asset.value))

def delete(self, name: str, *, environment: str = DEFAULT_ENV) -> bool:
"""Delete an asset; return whether it existed."""
removed = self._data.get(environment, {}).pop(name, None) is not None
if removed:
self._flush()
return removed

def list(self, *, environment: Optional[str] = None) -> List[Asset]:
"""List assets, optionally restricted to one ``environment``."""
envs = [environment] if environment else list(self._data)
return [
Asset(name, str(rec["type"]), env, rec["value"])
for env in envs
for name, rec in self._data.get(env, {}).items()
]


def store_set(name: str, value: Any, *, asset_type: str = TYPE_TEXT,
environment: str = DEFAULT_ENV,
db: Optional[str] = None) -> Dict[str, Any]:
"""Set an asset and return a result dict (shared by executor/MCP layers)."""
AssetStore(db).set(name, value, asset_type=asset_type,
environment=environment)
return {"ok": True, "name": name, "environment": environment}


def store_get(name: str, *, environment: str = DEFAULT_ENV,
db: Optional[str] = None) -> Dict[str, Any]:
"""Get an asset as a result dict (credential value stays a reference)."""
asset = AssetStore(db).get(name, environment=environment)
return {"name": asset.name, "type": asset.type, "value": asset.value}


def store_list(*, environment: Optional[str] = None,
db: Optional[str] = None) -> Dict[str, Any]:
"""List assets as a result dict of ``{name, type, environment}`` (no values)."""
assets = AssetStore(db).list(environment=environment)
return {"assets": [{"name": a.name, "type": a.type,
"environment": a.environment} for a in assets]}
Loading
Loading