agentacctacct
§ · Nested detail page

本地控制面 深度解析

本地控制面 模块拆解:从真实源码行号看 agentacct 如何把本地证据、隐私边界和控制面分开。

§ 00 · IMPORT CHAIN

导入链与契约

control plane 子页面看 dashboard 为什么是产品投影而不是进程控制台。src/agent_chronicle/control_web.py:50 到 src/agent_chronicle/control_web.py:178 复制任务、合同、agent、workspace、attempt 和 approval 的安全字段,同时明确 external_codex 与 external_claude_code 只是 observed_only。tests/test_control_api.py:81 到 tests/test_control_api.py:120 验证路径、argv、PID、nonce、manifest id 都不出现在 API 响应里。 tests/test_control_api.py:123,这里再看 tests/test_control_api.py:151

src/agent_chronicle/control_web.py · L50-178Python
50def sanitize_control_projection(
51    projection: ControlProjection,
52    *,
53    observed_tasks: Sequence[Mapping[str, Any]] = (),
54) -> dict[str, Any]:
55    """Return the complete product projection without execution authority.
56
57    In particular this omits workspace roots/store paths, argv templates,
58    process ids/groups/birth times, executable/cwd, ownership nonce hashes,
59    manifest ids, event payloads/hashes, and arbitrary failure strings.
60    """
61
62    tasks = [item.to_dict() for item in sorted(projection.tasks.values(), key=lambda row: row.task_id)]
63    contracts = [
64        {
65            "task_id": item.task_id,
66            "revision": item.revision,
67            "objective": item.objective,
68            "workspace_id": item.workspace_id,
69            "mutation_mode": str(item.permission_envelope.get("mutation_mode") or "not_recorded"),
70            "launch_approval_required": contract_requires_launch_approval(item.permission_envelope),
71            "budget_policy_ids": list(item.budget_policy_ids),
72            "success_checks": list(item.success_checks),
73            "created_at": item.created_at,
74        }
75        for item in sorted(projection.contracts.values(), key=lambda row: row.task_id)
76    ]
77    agents = [
78        {
79            "agent_id": item.agent_id,
80            "display_name": item.display_name,
81            "adapter": item.adapter,
82            "execution_backend": item.execution_backend,
83            "capabilities": list(item.capabilities),
84            "command_registered": bool(item.argv_template),
85            "enabled": item.enabled,
86            "revision": item.revision,
87            "updated_at": item.updated_at,
88        }
89        for item in sorted(projection.agents.values(), key=lambda row: row.agent_id)
90    ]
91    workspaces = [
92        {
93            "workspace_id": item.workspace_id,
94            "enabled": item.enabled,
95            "revision": item.revision,
96            "updated_at": item.updated_at,
97        }
98        for item in sorted(projection.workspaces.values(), key=lambda row: row.workspace_id)
99    ]
100    attempt_created_at = {
101        item.target_id: item.occurred_at
102        for item in projection.events
103        if item.action == "attempt_created"
104    }
105    attempts = [
106        _safe_attempt(item, created_at=attempt_created_at.get(item.attempt_id))
107        for item in sorted(
108            projection.attempts.values(),
109            key=lambda row: _stamp(attempt_created_at.get(row.attempt_id) or row.started_at or row.ended_at),
110            reverse=True,
111        )
112    ]
113    approvals = [
114        {
115            "approval_id": item.approval_id,
116            "task_id": item.task_id,
117            "attempt_id": item.attempt_id,
118            "kind": item.kind,
119            "requested_action": item.requested_action,
120            "state": item.state,
121            "expires_at": item.expires_at,
122            "decided_at": item.decided_at,
123            "consumed_at": item.consumed_at,
124            "revision": item.revision,
125        }
126        for item in sorted(projection.approvals.values(), key=lambda row: row.expires_at)
127    ]
128    budgets = [item.to_dict() for item in sorted(projection.budget_policies.values(), key=lambda row: row.policy_id)]
129    schedules = [item.to_dict() for item in sorted(projection.schedules.values(), key=lambda row: row.schedule_id)]
130    events = [
131        {
132            "event_id": item.event_id,
133            "occurred_at": item.occurred_at,
134            "action": item.action,
135            "target_type": item.target_type,
136            "target_id": item.target_id,
137            "prior_state": item.prior_state,
138            "next_state": item.next_state,
139        }
140        for item in sorted(projection.events, key=lambda row: row.occurred_at, reverse=True)[:24]
141    ]
142    observed = [
143        {
144            "task_id": str(item.get("task_id") or ""),
145            "title": str(item.get("title") or "Observed Task"),
146        }
147        for item in observed_tasks
148        if str(item.get("task_id") or "").startswith("task_")
149    ]
150    active_execution = {"pending", "launching", "running", "cancel_requested"}
151    summary = {
152        "task_count": len(tasks),
153        "attempt_count": len(attempts),
154        "active_attempt_count": sum(row["execution_state"] in active_execution for row in attempts),
155        "running_attempt_count": sum(row["execution_state"] in {"launching", "running", "cancel_requested"} for row in attempts),
156        "pending_approval_count": sum(row["state"] == "pending" for row in approvals),
157    }
158    return {
159        "schema_version": CONTROL_WEB_SCHEMA_VERSION,
160        "summary": summary,
161        "tasks": tasks,
162        "contracts": contracts,
163        "agents": agents,
164        "workspaces": workspaces,
165        "attempts": attempts,
166        "approvals": approvals,
167        "budget_policies": budgets,
168        "schedules": schedules,
169        "events": events,
170        "issues": [{"code": item.code, "line_number": item.line_number} for item in projection.issues],
171        "observed_tasks": observed,
172        "authority_boundary": {
173            "owned_execution": "controllable",
174            "external_codex": "observed_only",
175            "external_claude_code": "observed_only",
176            "external_orchestrators": "observed_only",
177        },
178    }
§ 01 · CORE IMPL

核心实现,逐段拆

▸安全投影:没有执行权柄

src/agent_chronicle/control_web.py:181 到 src/agent_chronicle/control_web.py:205 展示第一层实现。这里不是抽象概念,而是把边界条件写成代码路径:调用方只能沿这些函数和常量进入,异常和漂移被计数或拒绝。 src/agent_chronicle/control_web.py:184,这里再看 src/agent_chronicle/control_web.py:190,这里再看 src/agent_chronicle/control_web.py:203

src/agent_chronicle/control_web.py · L181-205Python
181def _hidden_fields(csrf_token: str, *, expected_revision: int | None = None) -> str:
182    revision = (
183        f'<input type="hidden" name="expected_revision" value="{int(expected_revision)}">'
184        if expected_revision is not None
185        else ""
186    )
187    return (
188        f'<input type="hidden" name="csrf_token" value="{_esc(csrf_token)}">'
189        f'<input type="hidden" name="idempotency_key" value="web:{secrets.token_hex(20)}">'
190        + revision
191    )
192
193
194def _badge(value: Any) -> str:
195    text = str(value or "unknown").replace("_", " ")
196    normalized = str(value or "unknown").lower()
197    if normalized in {"ready", "running", "succeeded", "verified", "approved", "consumed"}:
198        css = "status-ready"
199    elif normalized in {"failed", "lost", "rejected", "control_failure", "finding"}:
200        css = "status-error"
201    elif normalized in {"pending", "launching", "cancel_requested", "awaiting_approval", "policy_hold"}:
202        css = "status-finding"
203    else:
204        css = "status-missing"
205    return f'<span class="status {css}">{_esc(text.title())}</span>'

▸表单只创建待审合同

src/agent_chronicle/control_web.py:208 到 src/agent_chronicle/control_web.py:259 是第二层实现,重点在失败路径和原子性。页面关注的是这些分支如何避免把未验证输入提升为 authority。 src/agent_chronicle/control_web.py:212,这里再看 src/agent_chronicle/control_web.py:220,这里再看 src/agent_chronicle/control_web.py:256

src/agent_chronicle/control_web.py · L208-259Python
208def _task_form(payload: Mapping[str, Any], csrf_token: str) -> str:
209    agents = [
210        row
211        for row in payload.get("agents", [])
212        if isinstance(row, Mapping)
213        and row.get("enabled")
214        and row.get("adapter") == "local_argv"
215        and row.get("execution_backend") == "subprocess"
216    ]
217    workspaces = [row for row in payload.get("workspaces", []) if isinstance(row, Mapping) and row.get("enabled")]
218    if not agents or not workspaces:
219        return """<section class="section control-setup">
220          <div class="section-header"><div><h2>Register owned execution</h2><p>agentacct needs one explicit workspace and one fixed argv adapter before it can start anything.</p></div></div>
221          <div class="control-command-grid">
222            <div><strong>1 · Register this workspace</strong><code>agentacct control register-workspace --store-dir .agent-sentinel/state --root . --workspace-id project</code></div>
223            <div><strong>2 · Register a local argv adapter</strong><code>agentacct control register-agent --store-dir .agent-sentinel/state --agent-id local-agent --display-name &quot;Local agent&quot; --argv-json '[&quot;/absolute/command&quot;,&quot;arg&quot;]'</code></div>
224          </div>
225          <p class="section-note">Registration does not adopt a running Codex, Claude Code, or orchestrator process. It creates a separate agentacct-owned launch definition.</p>
226        </section>"""
227    task_options = ['<option value="">Create a new planned Task</option>']
228    for row in payload.get("observed_tasks", []):
229        if isinstance(row, Mapping):
230            task_options.append(
231                f'<option value="{_esc(row.get("task_id"))}">{_esc(row.get("title") or "Observed Task")}</option>'
232            )
233    agent_options = "".join(
234        f'<option value="{_esc(row.get("agent_id"))}">{_esc(row.get("display_name") or row.get("agent_id"))}</option>'
235        for row in agents
236    )
237    workspace_options = "".join(
238        f'<option value="{_esc(row.get("workspace_id"))}">{_esc(row.get("workspace_id"))}</option>'
239        for row in workspaces
240    )
241    budget_options = '<option value="">No agentacct budget policy</option>' + "".join(
242        f'<option value="{_esc(row.get("policy_id"))}">{_esc(row.get("policy_id"))} · {_esc(row.get("action"))}</option>'
243        for row in payload.get("budget_policies", [])
244        if isinstance(row, Mapping) and row.get("enabled")
245    )
246    return f"""<section class="section control-create">
247      <div class="section-header"><div><h2>Plan an owned attempt</h2><p>Create a reviewable contract first. Nothing starts until you press Launch.</p></div></div>
248      <form class="control-task-form" method="post" action="/control/tasks">
249        {_hidden_fields(csrf_token)}
250        <label><span>Task</span><select name="task_id">{''.join(task_options)}</select><small>Choose an observed Task only when you explicitly want its next attempt governed here.</small></label>
251        <label class="control-wide"><span>Objective</span><input name="objective" maxlength="4000" required placeholder="What should this attempt accomplish?"></label>
252        <label><span>Workspace</span><select name="workspace_id" required>{workspace_options}</select></label>
253        <label><span>Agent adapter</span><select name="agent_id" required>{agent_options}</select></label>
254        <label><span>Mutation boundary</span><select name="mutation_mode"><option value="read_only">Read only</option><option value="workspace_write">Workspace write · approval required</option></select></label>
255        <label><span>Budget policy</span><select name="budget_policy_id">{budget_options}</select></label>
256        <label class="control-wide"><span>Success checks</span><textarea name="success_checks" maxlength="4000" placeholder="One objective check per line"></textarea></label>
257        <div class="control-wide control-form-action"><p>agentacct freezes this contract and local launch identity. Agent-native permissions still apply.</p><button class="button" type="submit">Create pending attempt</button></div>
258      </form>
259    </section>"""
§ 02 · KEY FLOW

关键流程

registered workspace/agent ─▶ contract ─▶ pending attempt ─▶ launch button ─▶ sanitized control JSON

▸实现判断

这里的设计重心是“先冻结合同,再启动 owned subprocess”。UI 里出现的 button 并不意味着它能控制外部 Codex;只有由 agentacct 注册的 local_argv adapter 才进入可启动集合。 这条路径还可以和主页面的 src/agent_chronicle/source_discovery.py:37、src/agent_chronicle/capture_runtime.py:41、src/agent_chronicle/control_web.py:50 连起来看:agentacct 的策略是收集证据但不收集敏感正文,展示状态但不泄露执行权柄。

§ 03 · EDGE CASES

边界 / 失败模式

  • src/agent_chronicle/control_web.py:50 附近的入口如果收到不可信输入,只能产出降级或拒绝,不得写入 raw prompt/response。
  • src/agent_chronicle/control_web.py:181 的分支必须和测试夹具保持同步,避免客户端格式漂移后静默误归因。
  • src/agent_chronicle/control_web.py:259 附近的失败路径需要持续保留计数或错误码,方便 dashboard 暴露健康状态。
Sub-page · 2026-07-27 Source · mikehasa/agentacct ↑ agentacct 主页面