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-22) — Cookie Jar (HTTP Session Carry)](#whats-new-2026-06-22--cookie-jar-http-session-carry)
- [What's new (2026-06-22) — HTTP Content Negotiation & Decompression](#whats-new-2026-06-22--http-content-negotiation--decompression)
- [What's new (2026-06-22) — multipart/form-data Build & Parse](#whats-new-2026-06-22--multipartform-data-build--parse)
- [What's new (2026-06-22) — Secret Redaction for Config & Logs](#whats-new-2026-06-22--secret-redaction-for-config--logs)
Expand Down Expand Up @@ -143,6 +144,12 @@

---

## What's new (2026-06-22) — Cookie Jar (HTTP Session Carry)

Carry a session across HTTP calls. Full reference: [`docs/source/Eng/doc/new_features/v91_features_doc.rst`](docs/source/Eng/doc/new_features/v91_features_doc.rst).

- **`CookieJar` / `parse_set_cookie`** (`AC_cookie_header`, `AC_parse_set_cookie`): `http_request` is stateless — no session cookies persisted across calls, so a login-then-call flow couldn't carry a session headlessly. This parses `Set-Cookie` headers into a jar, builds the `Cookie` request header, and saves/loads the jar as JSON (cookies cleared on `Max-Age<=0`/empty). Pure-stdlib, deterministic.

## What's new (2026-06-22) — HTTP Content Negotiation & Decompression

Build `Accept` headers and decode gzip/deflate. Full reference: [`docs/source/Eng/doc/new_features/v90_features_doc.rst`](docs/source/Eng/doc/new_features/v90_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-22) — Cookie Jar(HTTP 会话携带)](#本次更新-2026-06-22--cookie-jarhttp-会话携带)
- [本次更新 (2026-06-22) — HTTP 内容协商与解压](#本次更新-2026-06-22--http-内容协商与解压)
- [本次更新 (2026-06-22) — multipart/form-data 构建与解析](#本次更新-2026-06-22--multipartform-data-构建与解析)
- [本次更新 (2026-06-22) — 配置与日志的机密脱敏](#本次更新-2026-06-22--配置与日志的机密脱敏)
Expand Down Expand Up @@ -142,6 +143,12 @@

---

## 本次更新 (2026-06-22) — Cookie Jar(HTTP 会话携带)

跨 HTTP 调用携带会话。完整参考:[`docs/source/Zh/doc/new_features/v91_features_doc.rst`](../docs/source/Zh/doc/new_features/v91_features_doc.rst)。

- **`CookieJar` / `parse_set_cookie`**(`AC_cookie_header`、`AC_parse_set_cookie`):`http_request` 无状态 —— 没有会话 cookie 在调用间延续,login-then-call 流程无法在无头情况下携带会话。本功能把 `Set-Cookie` 标头解析进 jar、构建 `Cookie` 请求标头,并以 JSON 存/读 jar(`Max-Age<=0`/空值时清除)。纯标准库、确定。

## 本次更新 (2026-06-22) — HTTP 内容协商与解压

构建 `Accept` 标头并解码 gzip/deflate。完整参考:[`docs/source/Zh/doc/new_features/v90_features_doc.rst`](../docs/source/Zh/doc/new_features/v90_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-22) — Cookie Jar(HTTP 工作階段攜帶)](#本次更新-2026-06-22--cookie-jarhttp-工作階段攜帶)
- [本次更新 (2026-06-22) — HTTP 內容協商與解壓縮](#本次更新-2026-06-22--http-內容協商與解壓縮)
- [本次更新 (2026-06-22) — multipart/form-data 建立與解析](#本次更新-2026-06-22--multipartform-data-建立與解析)
- [本次更新 (2026-06-22) — 設定與日誌的機密遮蔽](#本次更新-2026-06-22--設定與日誌的機密遮蔽)
Expand Down Expand Up @@ -142,6 +143,12 @@

---

## 本次更新 (2026-06-22) — Cookie Jar(HTTP 工作階段攜帶)

跨 HTTP 呼叫攜帶工作階段。完整參考:[`docs/source/Zh/doc/new_features/v91_features_doc.rst`](../docs/source/Zh/doc/new_features/v91_features_doc.rst)。

- **`CookieJar` / `parse_set_cookie`**(`AC_cookie_header`、`AC_parse_set_cookie`):`http_request` 無狀態 —— 沒有工作階段 cookie 在呼叫間延續,login-then-call 流程無法在無頭情況下攜帶工作階段。本功能把 `Set-Cookie` 標頭解析進 jar、建立 `Cookie` 請求標頭,並以 JSON 存/讀 jar(`Max-Age<=0`/空值時清除)。純標準函式庫、具決定性。

## 本次更新 (2026-06-22) — HTTP 內容協商與解壓縮

建立 `Accept` 標頭並解碼 gzip/deflate。完整參考:[`docs/source/Zh/doc/new_features/v90_features_doc.rst`](../docs/source/Zh/doc/new_features/v90_features_doc.rst)。
Expand Down
41 changes: 41 additions & 0 deletions docs/source/Eng/doc/new_features/v91_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Cookie Jar (HTTP Session Carry)
===============================

``http_request`` is stateless — no session cookies persist across calls, so a
login-then-call REST flow could not carry a session headlessly. This parses
``Set-Cookie`` response headers into a jar and builds the ``Cookie`` request
header; the jar is JSON-serialisable so a session can be saved and reloaded.

Pure standard library (``json``); imports no ``PySide6``. The jar is a simple
in-memory name-value store (cookies cleared on ``Max-Age<=0`` / empty value),
so behaviour is fully deterministic in CI.

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

.. code-block:: python

from je_auto_control import CookieJar, parse_set_cookie

jar = CookieJar()
jar.update(login_response_set_cookie_headers) # str or list of Set-Cookie
cookie = jar.cookie_header() # "sid=abc; theme=dark"
# send `cookie` as the Cookie header on subsequent requests

jar.save("session.json")
jar = CookieJar.load("session.json")

``parse_set_cookie`` parses one ``Set-Cookie`` value into ``{name, value,
attributes}``. ``CookieJar.update`` applies one or many ``Set-Cookie`` headers
(removing a cookie on an empty value or ``Max-Age<=0``); ``set`` assigns
directly; ``cookie_header`` builds the request header; ``to_dict`` / ``from_dict``
and ``save`` / ``load`` persist the jar as JSON. (Domain/path matching is
simplified — this is a session-carry jar, not a full RFC 6265 policy engine.)

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

``AC_cookie_header`` builds ``{cookie_header, cookies}`` from one or many
``set_cookies``; ``AC_parse_set_cookie`` returns ``{cookie}`` for one header.
Both are exposed as MCP tools (``ac_cookie_header`` / ``ac_parse_set_cookie``)
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 @@ -113,6 +113,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/new_features/v90_features_doc
doc/new_features/v91_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
36 changes: 36 additions & 0 deletions docs/source/Zh/doc/new_features/v91_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Cookie Jar(HTTP 工作階段攜帶)
=============================

``http_request`` 無狀態 —— 沒有任何工作階段 cookie 會在呼叫間延續,因此 login-then-call 的 REST 流程
無法在無頭情況下攜帶工作階段。本功能把 ``Set-Cookie`` 回應標頭解析進一個 jar 並建立 ``Cookie`` 請求標頭;
jar 可序列化為 JSON,因此工作階段可存檔與重新載入。

純標準函式庫(``json``);不匯入 ``PySide6``。jar 為簡單的記憶體內名稱-值儲存(``Max-Age<=0`` / 空值時
清除 cookie),因此行為在 CI 中完全具決定性。

無頭 API
--------

.. code-block:: python

from je_auto_control import CookieJar, parse_set_cookie

jar = CookieJar()
jar.update(login_response_set_cookie_headers) # str 或 Set-Cookie 清單
cookie = jar.cookie_header() # "sid=abc; theme=dark"
# 後續請求把 `cookie` 當作 Cookie 標頭送出

jar.save("session.json")
jar = CookieJar.load("session.json")

``parse_set_cookie`` 把單一 ``Set-Cookie`` 值解析成 ``{name, value, attributes}``。``CookieJar.update``
套用一或多個 ``Set-Cookie`` 標頭(空值或 ``Max-Age<=0`` 時移除 cookie);``set`` 直接指定;``cookie_header``
建立請求標頭;``to_dict`` / ``from_dict`` 與 ``save`` / ``load`` 以 JSON 持久化 jar。(網域/路徑比對為簡化版
—— 這是工作階段攜帶用的 jar,而非完整 RFC 6265 政策引擎。)

執行器命令
----------

``AC_cookie_header`` 從一或多個 ``set_cookies`` 建立 ``{cookie_header, cookies}``;``AC_parse_set_cookie``
對單一標頭回傳 ``{cookie}``。兩者皆以 MCP 工具(``ac_cookie_header`` / ``ac_parse_set_cookie``)以及
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 @@ -113,6 +113,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/new_features/v90_features_doc
doc/new_features/v91_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
3 changes: 3 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@
build_accept, build_accept_encoding, decode_body, negotiated_call,
parse_quality_values,
)
# RFC 6265 cookie jar (carry a session across HTTP calls)
from je_auto_control.utils.cookie_jar import CookieJar, parse_set_cookie
# W3C Trace Context propagation (traceparent / tracestate)
from je_auto_control.utils.trace_context import (
SpanContext, TraceContextError, child_context, extract_context,
Expand Down Expand Up @@ -955,6 +957,7 @@ def start_autocontrol_gui(*args, **kwargs):
"MultipartFile", "build_multipart", "new_boundary", "parse_multipart",
"build_accept", "build_accept_encoding", "decode_body", "negotiated_call",
"parse_quality_values",
"CookieJar", "parse_set_cookie",
"SpanContext", "TraceContextError", "child_context", "extract_context",
"format_traceparent", "format_tracestate", "inject_context",
"new_root_context", "new_span_id", "new_trace_id", "parse_traceparent",
Expand Down
16 changes: 16 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,22 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Parse an Accept/Accept-Encoding header by q-value.",
))
specs.append(CommandSpec(
"AC_cookie_header", "Data", "Cookie Jar: Build Cookie Header",
fields=(
FieldSpec("set_cookies", FieldType.STRING,
placeholder='sid=abc; Path=/ (or ["a=1", "b=2"])'),
),
description="Build a Cookie request header from Set-Cookie value(s).",
))
specs.append(CommandSpec(
"AC_parse_set_cookie", "Data", "Cookie Jar: Parse Set-Cookie",
fields=(
FieldSpec("header", FieldType.STRING,
placeholder="sid=abc; Path=/; Max-Age=3600"),
),
description="Parse one Set-Cookie header into name/value/attributes.",
))
specs.append(CommandSpec(
"AC_resolve_config", "Data", "Layered Config: Resolve",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/cookie_jar/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""RFC 6265 cookie jar for AutoControl HTTP sessions."""
from je_auto_control.utils.cookie_jar.cookie_jar import (
CookieJar, parse_set_cookie,
)

__all__ = ["CookieJar", "parse_set_cookie"]
97 changes: 97 additions & 0 deletions je_auto_control/utils/cookie_jar/cookie_jar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""A small RFC 6265 cookie jar for carrying a session across HTTP calls.

``http_request`` is stateless — no session cookies persist across calls, so a
login-then-call REST flow could not carry a session headlessly. This parses
``Set-Cookie`` response headers into a jar and builds the ``Cookie`` request
header; the jar is JSON-serialisable so a session can be saved and reloaded.

Pure standard library (``json``); imports no ``PySide6``. The jar is a simple
in-memory name-value store (cookies cleared on ``max-age<=0`` / empty value), so
behaviour is fully deterministic in CI.
"""
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

SetCookie = Union[str, List[str]]


def parse_set_cookie(header: str) -> Optional[Dict[str, Any]]:
"""Parse one ``Set-Cookie`` value into ``{name, value, attributes}``."""
segments = [segment.strip() for segment in (header or "").split(";")]
if not segments or "=" not in segments[0]:
return None
name, _, value = segments[0].partition("=")
attributes: Dict[str, str] = {}
for segment in segments[1:]:
key, sep, attr_value = segment.partition("=")
attributes[key.strip().lower()] = attr_value.strip() if sep else ""
return {"name": name.strip(), "value": value.strip(),
"attributes": attributes}


def _is_expired(attributes: Dict[str, str]) -> bool:
max_age = attributes.get("max-age")
if max_age is not None:
try:
return int(max_age) <= 0
except ValueError:
return False
return False


class CookieJar:
"""An in-memory name-value cookie jar (RFC 6265, simplified)."""

def __init__(self, cookies: Optional[Dict[str, str]] = None) -> None:
self._cookies: Dict[str, str] = dict(cookies or {})

def update(self, set_cookie: SetCookie) -> "CookieJar":
"""Apply one or more ``Set-Cookie`` headers to the jar."""
headers = [set_cookie] if isinstance(set_cookie, str) else set_cookie
for header in headers:
self._apply_one(header)
return self

def _apply_one(self, header: str) -> None:
parsed = parse_set_cookie(header)
if parsed is None:
return
if not parsed["value"] or _is_expired(parsed["attributes"]):
self._cookies.pop(parsed["name"], None)
else:
self._cookies[parsed["name"]] = parsed["value"]

def set(self, name: str, value: str) -> "CookieJar":
"""Set a cookie value directly."""
self._cookies[str(name)] = str(value)
return self

def cookie_header(self) -> str:
"""Build the ``Cookie`` request header value from stored cookies."""
return "; ".join(f"{name}={value}"
for name, value in self._cookies.items())

def to_dict(self) -> Dict[str, str]:
"""Return the stored cookies as a plain dict."""
return dict(self._cookies)

def __len__(self) -> int:
return len(self._cookies)

@classmethod
def from_dict(cls, data: Dict[str, str]) -> "CookieJar":
"""Build a jar from a plain ``{name: value}`` dict."""
return cls(data)

def save(self, path: str) -> str:
"""Persist the jar to ``path`` as JSON; return the path."""
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(self._cookies, indent=2), encoding="utf-8")
return str(out)

@classmethod
def load(cls, path: str) -> "CookieJar":
"""Load a jar from a JSON file."""
return cls(json.loads(Path(path).read_text(encoding="utf-8")))
18 changes: 18 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3030,6 +3030,22 @@ def _redact_secret_text(text: str, mask: str = "***") -> Dict[str, Any]:
return {"text": redact_secret_text(text, mask=mask)}


def _cookie_header(set_cookies: Any) -> Dict[str, Any]:
"""Adapter: build a Cookie header from one/many Set-Cookie strings."""
import json
from je_auto_control.utils.cookie_jar import CookieJar
if isinstance(set_cookies, str) and set_cookies.strip().startswith("["):
set_cookies = json.loads(set_cookies)
jar = CookieJar().update(set_cookies)
return {"cookie_header": jar.cookie_header(), "cookies": jar.to_dict()}


def _parse_set_cookie(header: str) -> Dict[str, Any]:
"""Adapter: parse one Set-Cookie header into its components."""
from je_auto_control.utils.cookie_jar import parse_set_cookie
return {"cookie": parse_set_cookie(header)}


def _decode_body(headers: Any, body_base64: str) -> Dict[str, Any]:
"""Adapter: decode a Content-Encoding (gzip/deflate) base64 body."""
import base64
Expand Down Expand Up @@ -4354,6 +4370,8 @@ def __init__(self):
"AC_parse_multipart": _parse_multipart,
"AC_decode_body": _decode_body,
"AC_parse_quality_values": _parse_quality_values,
"AC_cookie_header": _cookie_header,
"AC_parse_set_cookie": _parse_set_cookie,
"AC_profile_rows": _profile_rows,
"AC_infer_schema": _infer_schema,
"AC_parse_problem": _parse_problem,
Expand Down
26 changes: 25 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,30 @@ def rate_limit_tools() -> List[MCPTool]:
]


def cookie_jar_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_cookie_header",
description=("Build a Cookie request header from one or many "
"'set_cookies' (Set-Cookie strings or a JSON list). "
"Returns {cookie_header, cookies}."),
input_schema=schema(
{"set_cookies": {"type": ["string", "array"]}},
["set_cookies"]),
handler=h.cookie_header,
annotations=READ_ONLY,
),
MCPTool(
name="ac_parse_set_cookie",
description=("Parse one Set-Cookie 'header' into {cookie: {name, "
"value, attributes}} (or null)."),
input_schema=schema({"header": {"type": "string"}}, ["header"]),
handler=h.parse_set_cookie,
annotations=READ_ONLY,
),
]


def http_content_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5272,7 +5296,7 @@ def media_assert_tools() -> List[MCPTool]:
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
dataset_diff_tools, referential_tools, link_header_tools, multipart_tools,
http_content_tools,
http_content_tools, cookie_jar_tools,
saga_tools, decision_table_tools, locator_repair_tools,
pii_text_tools, sarif_tools,
screen_record_tools,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,16 @@ def parse_quality_values(header):
return _parse_quality_values(header)


def cookie_header(set_cookies):
from je_auto_control.utils.executor.action_executor import _cookie_header
return _cookie_header(set_cookies)


def parse_set_cookie(header):
from je_auto_control.utils.executor.action_executor import _parse_set_cookie
return _parse_set_cookie(header)


def redact_config(obj, mask="***"):
from je_auto_control.utils.executor.action_executor import _redact_config
return _redact_config(obj, mask)
Expand Down
Loading
Loading