@multiagency/sdk 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGES.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -0
  4. package/THIRD-PARTY-NOTICES +127 -0
  5. package/bin/release-server.d.mts +29 -0
  6. package/bin/release-server.mjs +143 -0
  7. package/dist/compensation-contract.d.ts +370 -0
  8. package/dist/compensation-contract.js +51 -0
  9. package/dist/contract/agent-connection.d.ts +86 -0
  10. package/dist/contract/agent-connection.js +36 -0
  11. package/dist/contract/app-access.d.ts +141 -0
  12. package/dist/contract/app-access.js +57 -0
  13. package/dist/contract/app-client.d.ts +140 -0
  14. package/dist/contract/app-client.js +61 -0
  15. package/dist/contract/app-consent.d.ts +68 -0
  16. package/dist/contract/app-consent.js +91 -0
  17. package/dist/contract/assets.d.ts +6 -0
  18. package/dist/contract/assets.js +6 -0
  19. package/dist/contract/client.d.ts +347 -0
  20. package/dist/contract/client.js +81 -0
  21. package/dist/contract/gateway.d.ts +74 -0
  22. package/dist/contract/gateway.js +47 -0
  23. package/dist/contract/index.d.ts +26 -0
  24. package/dist/contract/index.js +25 -0
  25. package/dist/contract/inputs.d.ts +230 -0
  26. package/dist/contract/inputs.js +99 -0
  27. package/dist/contract/intents-account.d.ts +5 -0
  28. package/dist/contract/intents-account.js +8 -0
  29. package/dist/contract/orpc-client.js +2037 -0
  30. package/dist/contract/owner-capabilities.d.ts +81 -0
  31. package/dist/contract/owner-capabilities.js +33 -0
  32. package/dist/contract/owner-client.d.ts +99 -0
  33. package/dist/contract/owner-client.js +146 -0
  34. package/dist/contract/owner-contract.d.ts +1156 -0
  35. package/dist/contract/owner-contract.js +37 -0
  36. package/dist/contract/owner-inputs.d.ts +166 -0
  37. package/dist/contract/owner-inputs.js +1 -0
  38. package/dist/contract/owner-outputs.d.ts +5749 -0
  39. package/dist/contract/owner-outputs.js +751 -0
  40. package/dist/contract/package-version.d.ts +5 -0
  41. package/dist/contract/package-version.js +2 -0
  42. package/dist/contract/payee-entry.d.ts +10 -0
  43. package/dist/contract/payee-entry.js +10 -0
  44. package/dist/contract/payee.d.ts +326 -0
  45. package/dist/contract/payee.js +32 -0
  46. package/dist/contract/permissions.d.ts +20 -0
  47. package/dist/contract/permissions.js +64 -0
  48. package/dist/contract/proposals.d.ts +178 -0
  49. package/dist/contract/proposals.js +27 -0
  50. package/dist/contract/run-log-schema.d.ts +888 -0
  51. package/dist/contract/run-log-schema.js +150 -0
  52. package/dist/contract/run-progress-schema.d.ts +104 -0
  53. package/dist/contract/run-progress-schema.js +20 -0
  54. package/dist/payee-contract.d.ts +9 -0
  55. package/dist/payee-contract.js +15 -0
  56. package/package.json +58 -0
  57. package/provenance.json +61 -0
  58. package/schemas/agentConnection.json +76 -0
  59. package/schemas/agentConnectionIssue.json +22 -0
  60. package/schemas/agentConnectionList.json +88 -0
  61. package/schemas/agentConnectionStarted.json +90 -0
  62. package/schemas/compensationSend.json +40 -0
  63. package/schemas/errorBody.json +16 -0
  64. package/schemas/fileResult.json +67 -0
  65. package/schemas/invoicesView.json +130 -0
  66. package/schemas/ownerCompensationView.json +556 -0
  67. package/schemas/payeeCompensationView.json +546 -0
  68. package/schemas/payeeWorkOrderRequest.json +31 -0
  69. package/schemas/publicWorkOrder.json +94 -0
  70. package/schemas/reportCommand.json +82 -0
  71. package/schemas/requestRecord.json +149 -0
  72. package/schemas/session.json +20 -0
  73. package/schemas/workOrderList.json +43 -0
  74. package/schemas/workOrderRequestResult.json +20 -0
  75. package/skills/app-server/SKILL.md +190 -0
  76. package/skills/getting-started/SKILL.md +90 -0
  77. package/skills/owner-workflows/SKILL.md +157 -0
  78. package/skills/payee-workflows/SKILL.md +200 -0
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: owner-workflows
3
+ description: >
4
+ Use when building an owner-facing app or agent on @multiagency/sdk that reads a MultiAgency agent's records and
5
+ takes owner actions through the gateway: calling ownerClient procedures, following plan execution streams, preparing
6
+ an owner action for the wallet to sign and following its status, and recovering from refused, busy, unresolved,
7
+ provider, unavailable and unknown results without resending. Not for payees (use payee-workflows).
8
+ metadata:
9
+ purpose: 'Owner reads, owner-signed actions and money streams with ownerClient, with recovery by result kind and retained identities.'
10
+ type: core
11
+ library: '@multiagency/sdk'
12
+ library_version: '0.3.1'
13
+ requires:
14
+ - getting-started
15
+ sources:
16
+ - 'MultiAgency/platform:src/contract/owner-client.ts'
17
+ - 'MultiAgency/platform:src/contract/owner-contract.ts'
18
+ - 'MultiAgency/platform:src/contract/owner-outputs.ts'
19
+ ---
20
+
21
+ # Owner workflows with ownerClient
22
+
23
+ Read getting-started first for identities and result kinds. `ownerClient` runs in the owner's browser on the agent's
24
+ host, after the owner signed in (the gateway session cookie is sent same-origin). It never retries and never mints an
25
+ identity: every action you send carries an identity you keep.
26
+
27
+ ## Setup
28
+
29
+ ```ts
30
+ import { ownerClient } from "@multiagency/sdk";
31
+
32
+ // `base` is the agent's path on its host ("/a/<agent>"), or "" when the page is the agent's own.
33
+ export const owner = ownerClient({ base: "/a/payout" });
34
+ ```
35
+
36
+ Result: `await owner.call("instance")` answers `{ ok: true, value: { agentId, executor } }` for a signed-in owner;
37
+ `session` means sign in first, `forbidden` means this account has no access to this agent.
38
+
39
+ ## Read the agent's records
40
+
41
+ ```ts
42
+ import { ownerClient } from "@multiagency/sdk";
43
+
44
+ export async function heldPayouts(owner: ReturnType<typeof ownerClient>) {
45
+ // A read is safe to repeat once when its answer was lost; any other failure is shown as it is.
46
+ let held = await owner.call("held");
47
+ if (!held.ok && held.kind === "unavailable") held = await owner.call("held");
48
+ return held;
49
+ }
50
+ ```
51
+
52
+ ## Take an owner action: prepare, sign once, submit once, follow its status
53
+
54
+ The owner's wallet signs exactly the prepared intent. Keep `correlationId` from the moment `prepare` answers.
55
+
56
+ ```ts
57
+ import { ownerClient, type OwnerOutput } from "@multiagency/sdk";
58
+
59
+ // The wallet signs exactly `intent` and answers the intent with its signature fields added (the submit's `signedData`).
60
+ type Sign = (intent: NonNullable<OwnerOutput<"owner/prepare">["intent"]>) => Promise<Record<string, unknown>>;
61
+
62
+ export async function freezeAgent(owner: ReturnType<typeof ownerClient>, signWithWallet: Sign) {
63
+ const prepared = await owner.call("owner/prepare", { action: { type: "agent_freeze" }, signer: "wallet" });
64
+ if (!prepared.ok) return prepared; // nothing to sign yet; `unknown` keeps the action's identity
65
+ const { correlationId, intent } = prepared.value;
66
+ if (!intent) throw new Error("A wallet preparation always carries the intent to sign");
67
+ const submitted = await owner.call("owner/submit", { correlationId, signedData: await signWithWallet(intent) });
68
+ // Lost or unknown: do not sign or submit again. The correlation id finds the outcome.
69
+ if (submitted.ok || submitted.kind !== "unknown") return submitted;
70
+ return owner.call("owner/status", { correlationId });
71
+ }
72
+ ```
73
+
74
+ Result: `owner/submit` answers the provider status (`{ status: "SUCCESS" }` once applied). `owner/status` answers
75
+ `{ stage: "submitted", status }` once the outcome is recorded, `{ stage: "submitting" }` while it is still being
76
+ reconciled (read again later), and `{ stage: "unknown" }` for a correlation id this agent does not hold.
77
+
78
+ ## Follow a plan execution stream
79
+
80
+ `executePlan`, `rebalance`, `sweep` and `steward/propose` are streams. Events are validated one by one; the stream's
81
+ final value is `undefined` on success or a failure.
82
+
83
+ ```ts
84
+ import { ownerClient } from "@multiagency/sdk";
85
+
86
+ export async function execute(owner: ReturnType<typeof ownerClient>, planId: string) {
87
+ const events: unknown[] = [];
88
+ const stream = owner.stream("executePlan", { planId });
89
+ for (let next = await stream.next(); ; next = await stream.next()) {
90
+ if (next.done) return { events, failure: next.value };
91
+ events.push(next.value);
92
+ }
93
+ }
94
+ ```
95
+
96
+ A failure before the first event keeps its kind (`busy`, `unresolved`, `refused`, `provider`). A failure after any
97
+ event is `unknown` with `{ planId }`: part of the plan may have run. Read `activity` and `unresolved`; never execute the
98
+ plan again.
99
+
100
+ ## Outside a browser: Treasury's machine route
101
+
102
+ A Treasury wallet owner can issue a machine credential (`tmc_…`) from the owner's access screen. It works only at
103
+ `https://<issued host>/machine/v1/a/<agent>/rpc/*`, only for its scopes (read, prepare, execute-plan, sweep), and never
104
+ authenticates `/a/<agent>/rpc`, Payables owner procedures or a DAO's owner actions. The client adds no credential itself:
105
+ give it an absolute base and a `fetch` that adds the bearer.
106
+
107
+ ```ts
108
+ import { ownerClient } from "@multiagency/sdk";
109
+
110
+ // `origin` is the issued HTTPS host, `agent` the agent's name, `token` the credential; keep the token out of logs.
111
+ export function machineClient(origin: string, agent: string, token: string) {
112
+ const host = new URL(origin);
113
+ if (host.protocol !== "https:" || host.username || host.password || host.pathname !== "/" || host.search || host.hash)
114
+ throw new Error("Use the issued HTTPS origin, without credentials, path, query or fragment");
115
+ return ownerClient({ base: `https://${host.host}/machine/v1/a/${agent}`, fetch: (input, init) => fetch(input, { ...init,
116
+ headers: { ...Object.fromEntries(new Headers(init?.headers)), authorization: `Bearer ${token}` }, credentials: "omit", redirect: "error" }) });
117
+ }
118
+ ```
119
+
120
+ Result: `await machineClient(origin, "treasury", token).call("overview")` answers like the browser client. A procedure
121
+ outside the credential's scopes, or an expired or revoked credential, answers `forbidden` or `session`. Results, streams
122
+ and recovery are the same as above: a lost `executePlan` or `sweep` is `unknown`, and nothing is sent again.
123
+
124
+ ## Recover an action that returned no identity
125
+
126
+ `unknown` echoes the input as `identity`. That is a recovery identity only when the input carries one: `correlationId`
127
+ (`owner/submit`, `workOrders/submit`), `planId` (`executePlan`), a work order or invoice `id`, or a compensation
128
+ `commandId`. `pay`, `sweep` and `autopilot/tick` take none, and the server's operation key never reaches the caller when
129
+ the answer is lost. No read can then link the attempt to one record.
130
+
131
+ - Read `unresolved`. While it lists an operation, every new money action is refused, so nothing can be sent twice
132
+ meanwhile.
133
+ - Read `held`: a payout waiting for the owner's approval is open there, not in `unresolved`.
134
+ - Read `activity` for what settled or was refused since the attempt. A row with the same recipient and amount is not
135
+ proof that it is yours.
136
+ - Before acting again, have a person decide from those records; never resend automatically.
137
+
138
+ An `unresolved` row may carry `evidence`: the provider's answer when a claimed submission failed without a proven
139
+ outcome. It is diagnostic only. Its `requestId` identifies the failed request to quote to the provider, and
140
+ `providerIds` are unverified. Neither is a `correlationId`, neither may be passed to `paymentStatus`, and evidence never
141
+ resolves or clears the operation. `evidence` is absent from older servers and null when none was recorded or it was
142
+ unreadable.
143
+
144
+ ## Common mistakes
145
+
146
+ - **Resending after `unknown`** (CRITICAL): the provider may already have accepted it. Read `owner/status`,
147
+ `unresolved` or `activity` by the kept identity instead, or, for an action without one, as described above.
148
+ - **Branching on `error` text** (HIGH): messages change; `kind` and `code` are the contract.
149
+ - **Treating `forbidden` as an outage** (HIGH): it is this account's access. A DAO member without propose permission
150
+ cannot prepare owner actions; the owner's signature always comes from the wallet or a DAO vote.
151
+ - **Retrying money streams automatically** (CRITICAL): `executePlan`, `sweep` and `autopilot/tick` must never be wrapped
152
+ in library retries or refetching.
153
+
154
+ ## Completion
155
+
156
+ Each action you sent has a read outcome (`owner/status` final, or the operation listed in `activity`), and no
157
+ `unknown` remains unread. Report any outcome you could not read; do not act again.
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: payee-workflows
3
+ description: >
4
+ Use when a payee (contributor) app or the payee's own external agent works with a MultiAgency payee entry through
5
+ @multiagency/sdk/payee: submitting a compensation report, recovering a lost reply by the report's id, following the
6
+ report to its recorded payment, and starting, listing or revoking an agent connection that lets an agent outside a
7
+ browser report for the payee. Not for owner actions (use owner-workflows).
8
+ metadata:
9
+ purpose: 'Payee reports and their payment status with payeeClient, from a browser session or an agent connection, with recovery by kept identities.'
10
+ type: core
11
+ library: '@multiagency/sdk'
12
+ library_version: '0.3.1'
13
+ requires:
14
+ - getting-started
15
+ sources:
16
+ - 'MultiAgency/platform:src/contract/client.ts'
17
+ - 'MultiAgency/platform:src/contract/agent-connection.ts'
18
+ - 'MultiAgency/platform:src/compensation-contract.ts'
19
+ - 'MultiAgency/platform:docs/agent-access.md'
20
+ ---
21
+
22
+ # Payee workflows with payeeClient
23
+
24
+ Read getting-started first for identities and result kinds. A payee reaches only their own records on one payee entry.
25
+ A report records work, with an optional proposed amount; it never sends a payment and is not a money action. The owner reviews it, and the owner's own
26
+ signed steps pay it.
27
+
28
+ Commands that take a structured input (`signIn`, `requestWorkOrder`, `submitReport`, `startAgentConnection`) check it
29
+ with the exported schema before sending. Invalid input throws that schema's `ZodError` synchronously and sends nothing;
30
+ check it first with the same schema (`reportCommand.safeParse`, …) to show field messages. The client takes no abort
31
+ signal; a page that changes account or entry discards late answers itself.
32
+
33
+ ## Setup
34
+
35
+ In the payee's signed-in browser page (same-origin cookie):
36
+
37
+ ```ts
38
+ import { payeeClient } from "@multiagency/sdk/payee";
39
+
40
+ export const payee = payeeClient();
41
+ ```
42
+
43
+ In the payee's own agent, outside a browser, with a connection token the payee started (no cookie is sent):
44
+
45
+ ```ts
46
+ import { payeeClient } from "@multiagency/sdk/payee";
47
+
48
+ export const agentFor = (entry: string, token: string) => payeeClient({ base: entry, token });
49
+ ```
50
+
51
+ Result: `await client.compensationSession()` answers `{ ok: true, value: { account } }` for the payee's account.
52
+
53
+ ## Write, save and submit a report
54
+
55
+ A report belongs to one agreement the payee already holds; the owner records agreements, and a report cannot create
56
+ one. Every field is required except `amount`:
57
+
58
+ | Field | Meaning |
59
+ | --- | --- |
60
+ | `commandId` | This attempt's identity. Mint once per intended change; a changed payload under the same id is refused (`command_conflict`). |
61
+ | `id` | The request's identity. Mint once for a new report; keep it for every revision. |
62
+ | `expectedVersion` | `0` for a new request; for a revision, the request's current `version` as last read. |
63
+ | `submit` | `false` saves a server draft only the payee sees; `true` sends it to the owner for review. |
64
+ | `report.agreementId` | The agreement it bills against. A request keeps its first agreement. |
65
+ | `report.start`, `report.end` | `YYYY-MM-DD`, inside the agreement's period. |
66
+ | `report.contributions`, `report.assessment` | What was done, and the payee's own assessment. |
67
+ | `report.evidence` | Up to 12 HTTPS links, possibly none; the whole report stays within 12 KB. |
68
+ | `report.amount` | Optional proposed amount in atomic USDC (`"12000000"` is 12 USDC). The owner approves an amount separately. |
69
+
70
+ Keep the command durably (in your product's storage) before sending it. After `unknown`, read the request by the `id`
71
+ you kept. The read shows the command took effect only when the request's `version` passed the command's
72
+ `expectedVersion`: a revision's request already exists at the old version if the lost revision was never applied. Until
73
+ then the outcome stays unknown. The entry records each `commandId` with its content, so the person may send the identical
74
+ command again: it is applied once, or answers with the result already recorded. Never mint a new `commandId` or `id`
75
+ after `unknown`, and never retry automatically. The one case for a new `commandId` is a refusal with `stale_request`:
76
+ read the current version and make a new revision on it.
77
+
78
+ ```ts
79
+ import { payeeClient, reportCommand, type CompensationRequest } from "@multiagency/sdk/payee";
80
+
81
+ type Client = ReturnType<typeof payeeClient>;
82
+ type Command = Parameters<Client["submitReport"]>[0];
83
+ type Report = Command["report"];
84
+ /** Your product's durable storage for commands. The SDK supplies none. */
85
+ export type Retain = (command: Command) => Promise<void>;
86
+
87
+ /** A new request on an agreement the payee holds: fresh identities and expectedVersion 0. Sends nothing. */
88
+ export async function newReport(client: Client, agreementId: string, fields: Omit<Report, "agreementId">, submit: boolean) {
89
+ const view = await client.compensation();
90
+ if (!view.ok) return view;
91
+ const agreement = view.value.agreements.find(a => a.id === agreementId);
92
+ if (!agreement) return { ok: false as const, kind: "invalid" as const, error: "Choose one of this payee's agreements." };
93
+ if (fields.start < agreement.terms.start || fields.end > agreement.terms.end)
94
+ return { ok: false as const, kind: "invalid" as const, error: `Report inside ${agreement.terms.start} to ${agreement.terms.end}.` };
95
+ return checked({ commandId: crypto.randomUUID(), id: crypto.randomUUID(), expectedVersion: 0, submit, report: { ...fields, agreementId } });
96
+ }
97
+
98
+ /** A revision of a saved draft or a request returned for changes: the same id, its current version, a new commandId. */
99
+ export function revision(current: CompensationRequest, report: Omit<Report, "agreementId">, submit: boolean) {
100
+ if (current.state !== "draft" && current.state !== "changes_requested")
101
+ return { ok: false as const, kind: "invalid" as const, error: "Only a draft or a request returned for changes can be revised." };
102
+ return checked({ commandId: crypto.randomUUID(), id: current.id, expectedVersion: current.version, submit,
103
+ report: { ...report, agreementId: current.report.agreementId } });
104
+ }
105
+
106
+ function checked(command: Command) {
107
+ const parsed = reportCommand.safeParse(command);
108
+ return parsed.success ? { ok: true as const, value: parsed.data }
109
+ : { ok: false as const, kind: "invalid" as const, error: parsed.error.issues.map(issue => issue.message).join(" ") };
110
+ }
111
+
112
+ /**
113
+ * One send, only after the command is retained. A lost reply is followed by a read, never by another send: `applied` is
114
+ * true only when the request's version passed the command's expectedVersion. Another tab's revision also moves the
115
+ * version, so compare `request.report` with the command's when that matters.
116
+ */
117
+ export async function send(client: Client, command: Command, retain: Retain) {
118
+ await retain(command); // if this throws, nothing is sent
119
+ const sent = await client.submitReport(command);
120
+ if (sent.ok || sent.kind !== "unknown") return { sent, observed: null, applied: sent.ok };
121
+ const observed = await readRequest(client, command.id);
122
+ const version = observed.ok ? observed.value.request?.version ?? 0 : 0;
123
+ return { sent, observed, applied: version > command.expectedVersion };
124
+ }
125
+
126
+ /** Read only: the request, the owner's approved amount and the payment's recorded status. Approved is not paid. */
127
+ export async function readRequest(client: Client, requestId: string) {
128
+ const view = await client.compensation();
129
+ if (!view.ok) return view; // reading again is safe
130
+ const request = view.value.requests.find(r => r.id === requestId) ?? null;
131
+ const payment = view.value.payments.find(p => p.requestId === requestId) ?? null;
132
+ return { ok: true as const, value: { request, state: request?.state ?? null, proposed: request?.report.amount ?? null,
133
+ approved: request?.approvedAmount ?? null, payment: payment?.status ?? null, paid: payment?.status === "paid" } };
134
+ }
135
+ ```
136
+
137
+ Result: `newReport(…, false)` then `send` saves a draft (`state: "draft"`, `version: 1`). `revision(saved, report, true)`
138
+ then `send` submits it for review (`state: "submitted"`, `version: 2`). After `unknown`, `applied` says whether the read
139
+ shows it took effect; `false` keeps it unknown.
140
+
141
+ Refusals carry a `code` (status 409, or 404 for `not_found`): `stale_request` (the version moved: read it and revise on
142
+ the new version), `request_locked` (not a draft or returned request), `agreement_changed` (a request keeps its first
143
+ agreement), `outside_period`, `not_found`, `intake_full` (20 open requests per payee), `identity_conflict` (the `id`
144
+ belongs to an agreement), `command_conflict` (the `commandId` was used with other content), and `rate_limited` (429).
145
+ Evidence holds at most 12 HTTPS links, and the whole report at most 12 KB.
146
+
147
+ A request's `state` says who acts next:
148
+
149
+ | `state` | Next |
150
+ | --- | --- |
151
+ | `draft` | The payee submits it (a revision with `submit: true`). It isn't in review. |
152
+ | `submitted` | The owner reviews it. |
153
+ | `changes_requested` | The payee revises it; `feedback` is the owner's reason. |
154
+ | `approved` | The owner prepares and pays it. `approvedAmount` is the compensation, whatever amount was proposed. |
155
+ | `declined` | Final: no compensation. |
156
+
157
+ A payment appears in `payments` once the owner prepares one; `request.payment` names its mode. Its `status` comes from
158
+ the stored invoice record: `prepared`, `waiting`, `paying`, `paid`, `failed`, `refused`, `unknown` or `needs_review`
159
+ (`invoiceStatus` in this package also has `needs_agreement` and `busy`, which only a filing answers and a payment row
160
+ never shows). For an isolated order it may also be `preparing`, `expired` or
161
+ `unavailable` (the order couldn't be read); a shared payment whose invoice is missing is `preparation_incomplete`. Only
162
+ `paid` is settlement. `failed` means the provider reported the transfer failed or refunded. `unknown` and `needs_review`
163
+ are unresolved. None of these is a reason for the payee to file a replacement; the owner decides what follows.
164
+
165
+ ## Connect an agent (from the payee's browser session)
166
+
167
+ ```ts
168
+ import { payeeClient } from "@multiagency/sdk/payee";
169
+
170
+ export async function connectAgent(browser: ReturnType<typeof payeeClient>) {
171
+ const started = await browser.startAgentConnection({ label: "Report agent", ttlSeconds: 3600 });
172
+ if (!started.ok) return started; // a lost start is `unavailable`: list connections and revoke any orphan
173
+ // Give `token` to the agent once; it is never shown again. `permissions` is the whole of its reach.
174
+ return { ok: true as const, value: { token: started.value.token, permissions: started.value.connection.permissions } };
175
+ }
176
+
177
+ export async function revokeAll(browser: ReturnType<typeof payeeClient>) {
178
+ const listed = await browser.agentConnections();
179
+ if (!listed.ok) return listed;
180
+ for (const connection of listed.value.connections.filter(c => !c.revoked)) await browser.revokeAgentConnection(connection.id);
181
+ return { ok: true as const, value: listed.value.connections.length };
182
+ }
183
+ ```
184
+
185
+ An agent connection permits `compensation.read` and `compensation.submit` only. With the token, every other route is
186
+ `forbidden`, and an expired, revoked or wrong-entry token is `session`. Connections are managed from the browser
187
+ session only: a request carrying a token cannot start, list or revoke one.
188
+
189
+ ## Common mistakes
190
+
191
+ - **A new `commandId` or `id` after `unknown`** (CRITICAL): it creates a second report. Read by the kept `id`.
192
+ - **Using an agent connection to file invoices or work orders** (HIGH): filing stays with the person; the connection
193
+ answers `forbidden`.
194
+ - **Reading "approved" as paid** (HIGH): only a `paid` payment status is settlement.
195
+ - **Copying the payee's cookie or wallet key into an agent** (CRITICAL): start an agent connection instead.
196
+
197
+ ## Completion
198
+
199
+ The report's record is read by its `id`, and its payment status is the last value read. If a result stays `unknown`
200
+ or `unavailable`, report it with the kept `commandId` and `id`; do not submit a replacement.