easynet.run · 全部问题场景

回家后调用办公室的一步操作,不必接管整台电脑

晚上补来一份销售表,明早就要交报告。你已经回家,办公室却留着同事维护的处理程序。现在你得开远程桌面、传文件、找窗口、等导出,再把结果拿回来。其实要借用的,只是那一次处理。

先用三行销售数据看清两端各做什么:家里读取 input.csv,把文本交给办公室的 build_sales_report;办公室验表、计算金额、排成 Markdown,家里收到后保存为 report.md。下面是完整代码,输入和结果都能逐项核对。

这个样例没有办公室独有的价格表,也没有税费规则,只演示一次处理如何交给另一端。若你的程序可以方便地在家里运行,就不必远程调用;已有程序需要留在办公室,或由同事统一维护时,才值得接上这条连接。

  1. 01

    家里读取小表

    发送 CSV 文本,不发送本地路径

  2. 02

    核对真实发布信息

    owner 与 descriptor 来自提供方

  3. 03

    办公室执行

    只调用 build_sales_report

  4. 04

    回家取回结果

    核对报告,再保存新文件

先生成并检查任务;只有收到远端报告才算执行完成。

从真实发布信息准备 Mission

在已检查的示例目录中执行。descriptor.json 必须是实际 ability show 的输出;需要兼容的已安装 Runtime。这里只生成和编译,不运行远端任务。

Runtime 命令 · bash
python3 prepare_mission.py --descriptor descriptor.json \
  --input input.csv --output Mission.eal
easynet mission compile Mission.eal --emit-ir

预期结果:生成新 Mission.eal 并取得编译结果;编译成功不是远端执行证据,跨 Runtime 路径仍在复测。

以这份三行销售表为例。金额以美分填写:3 本笔记本、10 支笔、4 个文件夹,报告应得到 USD 82.50。这里不计算税费或汇率。

家里:提交 input.csv 的内容

product,quantity,unit_price_cents
Notebook,3,1250
Pen,10,250
Folder,4,500

办公室:生成并返回报告文本

# Sales report

| Product | Quantity | Amount (USD) |
| --- | ---: | ---: |
| Notebook | 3 | 37.50 |
| Pen | 10 | 25.00 |
| Folder | 4 | 20.00 |

Total: USD 82.50

右侧内容由下方 report.py 在本地运行得出,并与下载的 output.md 逐字比对。远程调用还需要分别接通提供方和调用方;这里没有把本地计算当成双设备实录。

办公室电脑只发布这一项处理

report.py 检查列名、行数与金额,用整数美分求和,再排成 Markdown。provider.py 将它注册为 build_sales_report;不接受磁盘路径、命令或任意模板。

把 Notebook 的数量改成 0,程序会拒绝生成报告:quantity must be an integer between 1 and 10000。本例的商品名只接受英文字母、数字、空格和少量标点;不接受中文或日文名称、退货负数或额外列。

"""Run on the provider device after its EasyNet Runtime is configured."""

from easyremote import ComputeNode
from report import sales_report

node = ComputeNode(namespace="office_report")


@node.register(description="Build a USD sales report from bounded CSV text; no file paths.")
def build_sales_report(csv_text: str) -> str:
    return sales_report(csv_text)


if __name__ == "__main__":
    node.serve()
展开完整 report.py
"""Synthetic sales report. Business logic only; no network or filesystem access."""

import csv
import io
import re


def sales_report(csv_text: str) -> str:
    """Return Markdown from at most 100 rows and 32 KiB of CSV text."""
    if not isinstance(csv_text, str) or len(csv_text) > 32768:
        raise ValueError("CSV must be text of at most 32 KiB")
    if len(csv_text.encode("utf-8")) > 32768:
        raise ValueError("CSV must be text of at most 32 KiB")
    reader = csv.DictReader(io.StringIO(csv_text), strict=True)
    try:
        fieldnames = reader.fieldnames
    except csv.Error as error:
        raise ValueError("Malformed CSV header") from error
    if fieldnames != ["product", "quantity", "unit_price_cents"]:
        raise ValueError("Expected product,quantity,unit_price_cents")
    lines = ["# Sales report", "", "| Product | Quantity | Amount (USD) |", "| --- | ---: | ---: |"]
    total = 0
    count = 0
    try:
        for count, row in enumerate(reader, 1):
            if count > 100 or set(row) != {"product", "quantity", "unit_price_cents"}:
                raise ValueError("Expected at most 100 rows with exactly three fields")
            product = row["product"] or ""
            if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9 ._-]{0,59}", product):
                raise ValueError("Product must contain 1–60 plain label characters")
            values = []
            for field, ceiling in [("quantity", 10000), ("unit_price_cents", 100000000)]:
                text = row[field] or ""
                if not re.fullmatch(r"[0-9]{1,9}", text) or not 1 <= int(text) <= ceiling:
                    raise ValueError(f"{field} must be an integer between 1 and {ceiling}")
                values.append(int(text))
            quantity, price = values
            amount = quantity * price
            total += amount
            lines.append(f"| {product} | {quantity} | {amount // 100}.{amount % 100:02d} |")
    except csv.Error as error:
        raise ValueError("Malformed CSV") from error
    if count == 0:
        raise ValueError("At least one sales row is required")
    return "\n".join([*lines, "", f"Total: USD {total // 100}.{total % 100:02d}", ""])

家里的电脑负责读入与保存

caller.py 读取本机小表格,把文本发给指定的提供方,再将返回内容保存到一个新文件。实际 owner 和 descriptor 必须从发布结果取得,不能拿办公室电脑的名字代替,也不能借用它的账号。

展开 caller.py
"""Caller-side example; requires actual provider identity and descriptor from deployment."""

import argparse
from pathlib import Path

from easyremote import Client, FreshRoot, ResolvedTargetSubject, remote


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--owner-ura", required=True)
    parser.add_argument("--descriptor-ref", required=True)
    parser.add_argument("--input", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()
    if args.output.exists():
        parser.error("Output already exists; choose a new file")
    with args.input.open("rb") as source:
        payload = source.read(32769)
    if len(payload) > 32768:
        parser.error("CSV exceeds 32 KiB")
    with Client(namespace="office_report", invocation_policy=FreshRoot(ResolvedTargetSubject())) as client:

        @remote(client=client, owner_ura=args.owner_ura, descriptor_ref=args.descriptor_ref, timeout=30)
        def build_sales_report(csv_text: str) -> str: ...

        result = build_sales_report(payload.decode("utf-8"))
    if not isinstance(result, str) or len(result.encode("utf-8")) > 65536:
        raise ValueError("Provider returned an unexpected report")
    with args.output.open("x", encoding="utf-8") as output:
        output.write(result)
    print(f"Saved report to {args.output}")


if __name__ == "__main__":
    main()

把接入任务交给你的编程 Agent

复制这份 AGENTS.md,把本页地址一起交给编程 Agent,并告诉它当前是办公室电脑还是家里的电脑。它会先取得并检查附件,确认安装版本、身份和测试目录,再用这三行示例数据试跑。

代码、示例输入输出与 AGENTS.md 保留英文原文;界面语言切换不会改动附件。

下载 AGENTS.md
阅读完整交接任务
# Task: use an office sales-report function from a second device

The user wants to submit a small sales CSV from their caller device and receive
a Markdown report produced by the provider device. Keep the business code on
the provider. Do not replace this with remote desktop or arbitrary shell access.
The requested handoff is a Mission.eal created on the provider and executed
by the caller's own Runtime. A Python smoke test alone does not finish this task.

## Files and the result to reproduce

If the user supplied the example files with these instructions, inspect them
locally. Otherwise ask for the case-page URL; copied instructions alone do not
identify a download origin. Resolve `/examples/office-report/` against that
user-provided URL to obtain `report.py`, `provider.py`, `caller.py`, `input.csv`,
`output.md`, `test_report.py`, `prepare_mission.py` and `test_prepare_mission.py`.
Do not guess a production domain. A localhost
preview URL on one device is not reachable from another device as localhost;
ask for an approved reachable source or transfer the inspected example files.
Use a new user-approved working directory; do not overwrite an existing
AGENTS.md or existing report. Inspect files before running.

The synthetic input has Notebook (3 × 1250 cents), Pen (10 × 250 cents), and
Folder (4 × 500 cents). The expected total is USD 82.50. Prices are already in
USD cents; this example does not calculate tax, refunds or currency conversion.
Product labels use ASCII letters, digits, spaces and `. _ -`, start with a
letter or digit, and have at most 60 characters. Quantity must be 1–10000 and
unit_price_cents 1–100000000. Reject extra columns, zero or negative values;
do not silently sanitize the user's data. Replacing Notebook's quantity with
0 must produce `quantity must be an integer between 1 and 10000` locally.

## Establish which device you are on

Ask whether this is the provider or caller if it is not known. Confirm the other
device, its operator and allowed test network. Never copy private keys, bearer
tokens or personal credentials between devices. Do not deploy to an unapproved
host, change firewalls, or expose real sales records without explicit permission.

Read local repository instructions and the installed CLI help. Use compatible
EasyRemote, EasyNet-Cli and EasyNet-Axon versions. Do not assume the installed
package exposes the same interfaces as an arbitrary source checkout.

## Provider device

1. In an isolated directory run
   `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest test_report.py test_prepare_mission.py`.
   This verifies local calculation and file generation, not a remote connection.
2. Configure and pair the local Runtime using the version-matched official
   guide. Ask for missing user decisions; do not invent device IDs or authorities.
3. In the compatible EasyRemote environment run `python3 provider.py`. It
   registers `build_sales_report` under namespace `office_report`.
4. Keep the terminal open. Its `Local active` line contains the actual Ability
   URA; this does not yet prove the caller can discover it. In another terminal,
   read the provider's public identity with `easynet runtime status --json`
   (`pairing.device_ura`). Use these actual values, not a friendly device name:

   ```sh
   easynet ability show "$ABILITY_URA" --node "$PROVIDER_DEVICE_URA" --format json > descriptor.json
   ```

   Run this in the new working directory only. Inspect `name`, `owner_ura`,
   `ability_ura`, `descriptor_ref` and `input_schema` in the result. The name
   must be `office_report.build_sales_report`. Establish the permitted caller
   through the Runtime's authorization workflow; a descriptor is not permission.
   Share only public metadata, never credentials.
5. Keep the provider and its Runtime running for the test. State clearly who
   maintains them afterwards. This sample does not wake a sleeping computer.

## Optional Python smoke test on the caller

1. Configure the caller's own Runtime and identity; obtain permission for the
   specific published function. Verify the source/owner of the descriptor.
2. Inspect `caller.py`. It reads at most 32 KiB locally, sends CSV text, and saves
   the returned text into a new local file. It does not ask the provider to open
   a path on the caller's filesystem.
3. Run `python3 caller.py --help`, then invoke it with the actual owner URA and
   descriptor reference, `--input input.csv`, and a new `--output report.md`.
   Do not literally substitute guessed identifiers or reuse provider credentials.
4. Compare the returned report with `output.md`. Record provider and caller
   device identities, revisions, request/receipt identifiers and output, without
   secrets. Test invalid input, denied caller and provider offline separately.
5. A timeout is not proof that execution was cancelled. Do not blindly retry or
   report success without receiving and checking the output.

## Generate on the provider, execute on the caller

The intended deliverable is a Mission.eal file generated on one device, copied
to the caller device, compiled there, and executed against this provider.
As inspected on 2026-09-07, the current CLI supports:

```sh
easynet mission compile Mission.eal --emit-ir
easynet mission run Mission.eal --format json
easynet mission show <actual-run-id> --trace
```

There is no verified `mission add` or `mission install` command. Do not pass an
EAL file to `ability deploy`; that expects a different package structure.

The development source now supports an explicit remote Agent member call with
a separate `descriptor_ref` constraint. Do not assume an installed or published
Runtime contains this change. The caller's compiler and the Runtime executing
the Mission must both support it. The owner and method remain separate fields;
do not use a full Ability URA as the method name or guess the descriptor hash.

With the actual descriptor.json and inspected input.csv on the provider, run:

```sh
python3 prepare_mission.py --descriptor descriptor.json --input input.csv --output Mission.eal
```

The generator refuses to overwrite a file. It records the actual owner, method
and descriptor reference, embeds the CSV text, and sets a 30-second timeout
with no retries. Generation does not contact a Runtime or grant permission.
Transfer Mission.eal using the user's approved file-sharing method. The file
contains the CSV data: inspect it before sharing. Do not copy the provider's
environment, private keys or credentials.

On the caller, inspect the file and compile it with the command above. Check
that the IR targets the expected remote Agent and descriptor, uses exactly
the sample CSV, and has zero retries. If the compiler rejects this syntax,
stop and report the version and error. Do not turn it into a shell-command
wrapper and label that native EAL.

After approval, run the file once, take the actual run_id from the response,
and read its trace. Compare the emitted report byte-for-byte with output.md,
not just the displayed total. Match the child invocation and its terminal
receipt with the provider's invocation record. A failed step, missing output
or mismatched receipt is not completion. Never treat a successful compile as evidence of remote execution.

The website example's cross-Runtime Mission path is still being tested; do not
promise it works on the user's installed release. If execution fails, retain
the sanitized error and do not retry blindly. Test denied-caller and provider
offline behavior separately before using real data.

## Completion report

Distinguish: local report test; provider publication; real caller invocation;
EAL compilation; real two-device EAL execution. Mark only what was observed.
Do not present two processes on one computer as two physical devices. List any
remaining dependency, and leave user files and shared services intact.

办公室取得实际发布信息后,用下方命令生成任务文件,再交给家里的 Agent。Mission.eal 里包含本次表格和目标地址,不包含办公室的登录凭据。

python3 prepare_mission.py --descriptor descriptor.json --input input.csv --output Mission.eal
展开 prepare_mission.py
"""Prepare an office-report Mission from actual published metadata; does not execute it."""
import argparse
import json
from pathlib import Path

METHOD = "office_report.build_sales_report"
MAX_CSV_BYTES = 32768


def published_selection(metadata):
    if not isinstance(metadata, dict):
        raise ValueError("Expected ability show JSON object")
    fields = ("name", "owner_ura", "ability_ura", "descriptor_ref")
    if any(not isinstance(metadata.get(key), str) or not metadata[key].strip() for key in fields):
        raise ValueError("Need actual name, owner_ura, ability_ura and descriptor_ref from ability show")
    if metadata["name"] != METHOD:
        raise ValueError("Metadata is not the office_report.build_sales_report function")
    if metadata.get("admission_action") not in (None, "invoke"):
        raise ValueError("The report function requires RPC invocation")
    # This is consistency checking, not a replacement URA/descriptor parser.
    # The installed compiler must validate canonical owner/reference binding.
    if not metadata["descriptor_ref"].startswith(metadata["ability_ura"] + "@") or not metadata["descriptor_ref"].endswith("!invoke"):
        raise ValueError("Descriptor reference does not match the reported RPC ability")
    return metadata["owner_ura"], METHOD, metadata["descriptor_ref"]


def mission_source(metadata, csv_text):
    if not isinstance(csv_text, str) or len(csv_text.encode("utf-8")) > MAX_CSV_BYTES:
        raise ValueError("CSV must be UTF-8 text of at most 32 KiB")
    owner, method, pin = published_selection(metadata)
    q = json.dumps
    return ('mission "office-report-fixture" {\n'
            f'  let report = {q(owner)}.{q(method)}(csv_text: {q(csv_text)}) descriptor_ref {q(pin)} timeout 30 on_failure abort\n'
            '  emit "report" kind "answer" value report.output\n}\n')


def read_bounded(path, limit):
    with Path(path).open("rb") as source:
        payload = source.read(limit + 1)
    if len(payload) > limit:
        raise ValueError("Input exceeds allowed byte size")
    return payload.decode("utf-8")


def prepare(metadata_path, input_path, output_path):
    output_path = Path(output_path)
    if output_path.exists() or output_path.is_symlink():
        raise ValueError("Output already exists; choose a new Mission file")
    metadata = json.loads(read_bounded(metadata_path, 65536))
    source = mission_source(metadata, read_bounded(input_path, MAX_CSV_BYTES))
    # Exclusive creation also refuses a file created after the initial check.
    with output_path.open("x", encoding="utf-8") as output:
        output.write(source)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--descriptor", type=Path, required=True, help="Actual ability show --format json output")
    parser.add_argument("--input", type=Path, required=True, help="Local UTF-8 input.csv, at most 32 KiB")
    parser.add_argument("--output", type=Path, required=True, help="New Mission.eal; never overwrites")
    args = parser.parse_args()
    try:
        prepare(args.descriptor, args.input, args.output)
    except (ValueError, OSError) as error:
        # Do not echo raw metadata, inputs, identifiers or parser exception payloads.
        parser.exit(1, f"Mission preparation failed ({type(error).__name__}); check metadata, sizes and new output path.\n")
    print("Created Mission source. Run easynet mission compile <file> --emit-ir to validate it before execution.")


if __name__ == "__main__":
    main()

本例的 Mission.eal 跨 Runtime 执行还在验证。附件会让 Agent 先检查安装版本,再从实际发布结果生成任务文件;编译通过后,还要收到远端报告才能确认完成。现有命令是 mission compile 和 mission run,没有 mission add/install。

接入时的检查项
  • 提供方确实执行了指定转换,调用方没有重新安装处理环境。
  • 无权限的文件或参数是否被拒绝。
  • 设备断线时是否显示失败或未知状态而不是假成功。
源码与接入资料