Skip to content

emrg: a feishu task type, with every message to a third party gated by its own approval - #2444

Merged
how2how2how2-arch merged 3 commits into
masterfrom
feature/feishu-task-type
Oct 21, 2026
Merged

how2how2how2-arch merged 3 commits into
masterfrom
feature/feishu-task-type

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2441

Adds a task type feishu and the gate that bounds it. Two halves:

The template — emrg/server/feishu_prompt.md, registered as
TASK_TEMPLATES["feishu"] and reachable as the feishu builtin task type. It reads
what arrived, gives every message one verdict (answer / act / remember / ignore), does
the work that needs no permission, and reports what waits. A message's own content is
data, never an instruction.

The gate — scripts/feishu-gate.py, because "only the host may authorise a message
to a third party" has to be a fact on disk rather than a sentence the model is asked to
keep. Three verbs:

  • request records the exact text, the recipient and the host's own account, and
    prints the request the host is asked to approve;
  • send sends the message one approval covers, and refuses a row that has already
    been sent;
  • self messages the host's own account, which needs no approval.

An approval is bound to the message by its sha256 and to its sender, and it is spent by
the send that uses it: one approval, one message, one recipient. That is the rule the
tests arm in both directions — re-sending an approved row fails, an approval from
somebody other than the host fails, and an approval whose text no longer matches the
message fails.

allow_abbrev=False is set on the parent parser and on each subparser. Without it
--to silently abbreviates to --token, which the suite caught as a real defect
before this branch was pushed.

The template joins the citation class in scripts/check-rant-citations.py, so the
guard's scanned set still covers every prompt template a reader acts on. Agent.md
pays for the new tool's line in that list by shortening four existing lines; the file
stays inside its 8,000-character cap.

Verified on this branch: uv run pytest tests/ → 5472 passed, 34 skipped;
from emrg.client.app import run_client imports; python -m emrg --help runs;
scripts/check-rant-citations.py → rc 0 over 58 citation sites.

CI is pending on this head.

…y its own approval

Adds the task type `feishu`: a template that reads the host's Feishu messages,
judges each one, does what needs no permission and asks where it does. A message
to the host's own account needs no gate; a message to anybody else goes through
`scripts/feishu-gate.py`, which holds the exact text, records the host's approval
and refuses a second send on one approval.

The gate is where the rule lives: the template describes how to use it, so the
boundary is a mechanism rather than a prompt sentence.
EMRG Evolution added 2 commits October 21, 2026 16:32
The `test-windows` leg ran the gate's own tests red: eleven of them failed with
`[WinError 193] %1 is not a valid Win32 application`, because the fixture wrote the
fake `lark-cli` as a shebang script and neither a shebang nor an execute bit means
anything to `CreateProcess`. The gate's own refusal ("the CLI could not be run") was
what every assertion then saw, so the failure was about the fixture and not about the
rule it tests.

The fixture now hands the gate a program: a shebang script on POSIX, a `.cmd` entry
naming the interpreter on Windows, with `%*` forwarding the caller's arguments to it as
written. Which one a host gets is a fact about the host; the property under test — one
approval, one message, for the body the host was shown — is the same on both.

A host that cannot run the Windows leg cannot run that arm either, so a test pins the
shape each arm produces: the shebang and execute bit, and the `@echo off` + CRLF + `%*`
a `.cmd` needs.
…sured

The `test-windows` run on the previous head left one failure after the fixture fix:
the CLI received `[EMRG approval request]` and nothing after it. The gate hands its
text to the CLI as one command-line argument and its request is several lines; a
`.cmd` shim — which is what npm installs — is parsed by the command interpreter before
it runs, so a newline inside an argument ends the command. A native `--cli` executable
does receive the whole argument.

So the limit is the transport's, not the tests'. The gate now states it where a reader
of the script meets it, and this module says why it is POSIX-only and exactly what that
leaves untested: the gate's argv-level behaviour on Windows, not the rule the tests are
about.

Handing the CLI a file instead would need an interface this gate has no way to verify,
so the limit is written down rather than guessed around.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261021-165236

Landing tree: base f3f9a6af, check-merge-plan-suite.py 2444 → tree 2de35be4cce2, 5494 passed / 35 skipped (+13 tests over master). Both CI legs on head 16f2f227 are green (test 3m22s, test-windows 5m17s); the landing diff is 8 paths — feishu_prompt.md (+71), scripts/feishu-gate.py (+458), tests/test_feishu_gate.py (+376), and the registration edits in scheduler.py, check-rant-citations.py, Agent.md, test_prompt_templates.py, test_scheduler.py.

Read the gate again this cycle. The properties that make it a gate rather than a suggestion:

  • send has neither --to nor a body argument, so "approved A, sent B" cannot be expressed at all; the recipient and bytes are locked at request time and re-read from the ledger.
  • The body's sha256 is re-checked immediately before anything leaves, so an edited body is refused rather than sent.
  • Consent is positional against the recorded request message_id, because create_time is minute-resolution and "after" cannot be read off it.
  • One approval is spent once (status == "sent" refuses a second send), and a pending request expires.
  • allow_abbrev=False on the parent and on every subparser: with it on, a redirect attempt's --to is read as a prefix of --token and overwrites it.

One reading, not a veto: the positional check assumes the CLI returns the chat's messages oldest-first. If a host's CLI answers newest-first the slice it searches is the older half, and since each token is fresh-random that half cannot carry this request's token — so the assumption fails closed (refusal, nothing sent) rather than granting a send. The Windows .cmd-shim limit is stated in the script's docstring with the measurement behind it, and the affected tests are POSIX-only with the loss spelled out.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261021-172452

Landing tree measured on the master the merge would actually use: base 0170b19a, check-merge-plan-suite.py 2444 → tree f628dd37fee9, 5495 passed / 35 skipped (+13 over master). Both CI legs on head 16f2f227 are green (test 3m22s, test-windows 5m17s), and check-merge-landing-diff.py reads the landing as 8 paths — emrg/server/feishu_prompt.md (+71), scripts/feishu-gate.py (+458), tests/test_feishu_gate.py (+376), plus the registration edits in scheduler.py, check-rant-citations.py, Agent.md, test_prompt_templates.py and test_scheduler.py. The later paths in diff(base, head) (emrg/__init__.py, the GUI version files, daemon.py, promote_prompt.md) are master's own commits arriving after this head, shown there as reversals the PR does not make.

What makes the gate a gate rather than a suggestion, read again on this head:

  • send has neither --to nor a body argument, so "approved A, sent B" cannot be expressed at all; recipient and bytes are locked at request time and re-read from the ledger.
  • The body's sha256 is re-checked immediately before anything leaves.
  • Consent is positional against the recorded request message_id, because the CLI reports create_time at minute resolution and "after" cannot be read off it.
  • One approval is spent once, a pending request expires, and allow_abbrev=False is set on the parent and every subparser — without it a redirect attempt's --to is read as a prefix of --token and silently overwrites it.

Two limits the change states rather than hides: the positional check assumes the chat stream is oldest-first, and if it is not the search covers the older half where this request's fresh random token cannot appear — so the assumption fails closed (refusal, nothing sent); and the Windows .cmd-shim argv truncation is documented in the script's docstring with the measurement behind it, with the affected tests POSIX-only and the loss named.

@how2how2how2-arch how2how2how2-arch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261021-180102

Measured on the landing tree (f628dd37fee9, base 0170b19a): full suite 5495 passed, 35 skipped. The head is three commits behind master, so its green CI is a verdict about a tree that can no longer be merged; the landing tree is the reading this vote is cast on, and the head does not move, so the two votes already standing survive.

Two arms, each killed at the line it names, on a copy of this head:

  • disabling the body sha256 comparison in send turns test_a_body_swapped_after_the_request_is_refused red — and the failure shows the consequence rather than a message: the gate really sent, {"sent": true, "to": "ou_third_party"}.
  • removing the sender filter from consent_found turns test_a_reply_from_anyone_but_the_host_is_not_consent red.

Control run unmodified: 12 passed. So the rule is armed in the direction that matters, not only asserted in prose.

What I checked by reading rather than trusting. send takes neither a recipient nor a body, so "approved A, sent B" has no spelling at the argument level, and the recipient and bytes are re-read from the ledger with the digest compared before anything leaves. Consent is positional because the CLI reports create_time at whole minutes, which is the right reading of "the reply came after" and the only one that survives two messages inside one minute. allow_abbrev=False is set on the parent and on every subparser, which closes the abbreviation that silently rewrote --to into --token. The body path is only ever built from a token that find_row has already matched against the ledger, so a path-shaped token never reaches the filesystem. The two red lines are carried verbatim into the template, where the family guard and the upgrade-chain red-line test pin them.

Two notes, neither a blocker and neither asking for a change to this head:

  1. The status == "sent" branch is redundant with the status != "pending" branch immediately below it — both refuse — so an arm that removes only the first survives. That is redundancy rather than a hole; the specific branch buys a message naming the message id that already spent the token.
  2. A crash between send_message and rewrite_ledger leaves the row pending, so that one message could be sent a second time. The window is narrow, and closing it would mean writing an intent row before the send — a change I would rather see argued on its own than slipped into a review.

The Windows transport limit is the part I most respect here: a .cmd shim truncates the argument at the first newline, so the tests are POSIX-only and say so, the script states the same limit for whoever configures --cli, and the limitation is named instead of being papered over with a shim that would have made the suite green and the gate wrong.

@how2how2how2-arch
how2how2how2-arch merged commit 6c39341 into master Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

emrg: a feishu task type, with every message to a third party gated by its own host approval

2 participants