RoleConfig.confirm_policy was parsed but never acted on. Wire the three
strategies for how OTHERS' action requests are confirmed (the owner's own
requests always auto-run regardless):
- manual (default): ask the owner every time
- auto: run without asking — full trust — still fully audited
- remember: ask once per action verb, then reuse that decision (approve →
auto-run same verb; reject → auto-decline same verb)
- gateDecision(requester, verb) returns auto / ask / reject
- ApprovalRequest.Verb carries the action verb (remember key + audit)
- handleOwnerDecision caches the decision per verb under the remember policy
- applyRoleConfig fills opts.ConfirmPolicy; launchConnector wires + logs it
Tests: auto runs without asking, manual asks, remember reuses the 1st decision.
The digital twin previously only knew one action (todo.create). Add two more
owner-scoped write actions so it can act as a real stand-in:
- calendar.create → calendar/create_calendar_event (summary + start/end)
- doc.create → doc/create_document (name)
Both flow through the same gate: detected via [[ACTION:...]] marker, classified
as write (gated), approved/deferred/audited identically. The agent system hint
now lists all three action markers. A shared firstNonEmpty arg helper.
Tests: toPlannedAction maps each verb to the right product/tool, missing
required args degrade to not-an-action, and every action classifies as write.
When an approved action can't execute right now (most often the connector's
dws login is not the bot owner, so an owner-scoped action like creating a
personal todo fails), the request is no longer dropped with an opaque error.
Instead it is held and retried after the owner recovers.
- new 'deferred' state: an approved request whose execution didn't complete;
persisted like any other, distinct from terminal 'failed'
- on execution failure (auto-approve path and owner-approve path), mark
deferred instead of failed, and privately DM the owner what went wrong and
how to recover (log dws in as the bot owner, then reply 「重试」). The
requester is NOT told it failed — their task is held, not lost
- 「重试/恢复」owner keyword flushes the backlog: re-execute each deferred
request oldest-first; on success mark executed and send the requester the
outcome, on failure keep it deferred
- markDeferred / allDeferred on the gate; deferred counts as approved() so the
retry can still mark it executed
- tests: deferred-hold-then-retry end to end, isRetryWord
This is the graceful-degradation half of the digital twin: a request survives
an owner-offline / wrong-identity window and completes once the owner is back.
Builds on the text-approval gate with the digital-twin model the owner asked
for: the approval stays strictly between the bot and its owner, and every
action is auditable in DingTalk.
- private owner DM: an action request is no longer posted into the requester's
conversation; the bot DMs the OWNER privately (robot oToMessages/batchSend,
reusing the bot's own credentials) and the requester sees nothing until the
result. Decision matching moved from per-conversation to latestPending, since
the owner replies from their own 1:1 chat, not the request conversation.
- owner self auto-approve: when the requester IS the owner, asking is the
authorization — the action runs immediately with no second confirmation, but
is still recorded (auto_approved=true, decided_by=owner) for audit.
- online-sheet audit: optional --audit-sheet / --audit-sheet-tab appends one
row per terminal action (time, summary, requester, state, decided_by, auto,
error, id) to a DingTalk online sheet via sheet append_rows, reviewable in
DingTalk on any device — beyond the local approvals JSON.
- aiCardClient.sendOTOText: proactive 1:1 robot text send primitive.
- requester gets the final outcome (executed/failed/rejected) by 1:1 message;
the approval process itself is never exposed to them.
Tests: parseDecisionWord, latestPending, private-DM approve/reject,
owner self auto-approve, audit terminal records, classifier wiring intact.
The confirmation gate previously REQUIRED a hand-built interactive card
template (per app, manual button param binding in the card console) — and
without one the gate silently turned off. That manual step is too clunky to
be the default. Add a text-approval mode so the digital twin works with zero
card-platform setup: when --owner-user-id is set but no --approval-card-
template, the bot posts a plain confirmation and the owner replies 「同意」/
「拒绝」 in chat to decide.
- newTextApprovalOrchestrator: gate enabled on owner alone (no card sender)
- text mode is async, NOT blocking: handleReply posts the prompt and returns;
the owner's decision arrives as an ordinary inbound message captured by
handleOwnerDecision (blocking on Await would deadlock the per-conversation
worker that must also process that reply)
- gate.pendingForConv maps an owner reply (carries only the conversation) back
to its request; parseDecisionWord matches a WHOLE-message keyword so an
embedded 「同意」 never accidentally approves
- only the configured owner can decide; non-owner / non-keyword pass through
- devapp_connect: no card template now falls back to text mode instead of
disabling the gate
- existing card flow refactored into handleReplyCard, unchanged behavior
- tests: parseDecisionWord, pendingForConv, text-mode approve/reject,
non-owner rejected, non-decision pass-through
Sprint 2 item 1: wire the dws command read/write classifier into the
digital-twin approval gate. Previously any detected [[ACTION:...]] marker
was unconditionally routed through the owner-confirmation gate. Now the
gate consults ClassifyDwsCommand on the planned command: a read-class
action bypasses the gate and replies normally, while write — and Unknown,
per the CmdClass safety contract — keep the owner sign-off requirement.
- add classifyPlannedAction bridging plannedAction -> CmdClass (classifies
on LegacyPath, falls back to product + RPC tool verb)
- handleReply skips Submit/card/Await for a read-class action
- tests: classifyPlannedAction table + handleReply read-class bypass
(override-driven, exercises the wiring end to end)
Add the M2 confirmation gate (digital twin): an action request from a
group member is not executed directly. The bot sends an [Approve]/[Reject]
card to the owner over the existing Stream long-connection and only runs the
planned command after the owner approves.
- connect_approval.go: gate engine. ApprovalRequest model, Pending to
Approved/Rejected to Executed/Failed state machine, thread-safe store with
crash-safe atomic on-disk persistence (~/.dws/connect/<clientId>/approvals,
0600, restart-recoverable), Submit/Decide/Await/Get API, and the simplified
[[ACTION:...]] marker detection for execution-class requests.
- connect_approval_card.go: approvalCardSender boundary (DI-fakeable), the
card-callback decoder (approval id + decision from action params, OutTrackId
fallback), the orchestrator driving Submit to card to Await to execute to
reply, and the real DingTalk interactive-card sender.
- connect_stream.go / devapp_connect.go: minimal wiring. Register the card
callback router, decorate the prompt for action detection, route a marked
reply through the gate, --owner-user-id / --approval-card-template flags.
Tests: state machine + persistence/recovery, card-action decode, reject path
(no execution), and a no-network end-to-end approve chain. gofmt/vet/build/
test all green.