@mnemom/mnemom 0.17.34 → 0.17.36

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 (177) hide show
  1. package/dist/commands/agent-config.js +3 -0
  2. package/dist/commands/agent-goal.d.ts +8 -3
  3. package/dist/commands/agent-goal.js +22 -8
  4. package/dist/commands/agent-run.d.ts +14 -2
  5. package/dist/commands/agent-run.js +28 -7
  6. package/dist/commands/agent-share-render.js +1 -1
  7. package/dist/commands/agent-slack.js +32 -0
  8. package/dist/commands/durable-inbox.d.ts +51 -0
  9. package/dist/commands/durable-inbox.js +134 -0
  10. package/dist/commands/durable-questions.d.ts +64 -0
  11. package/dist/commands/durable-questions.js +161 -0
  12. package/dist/commands/scan.d.ts +13 -3
  13. package/dist/commands/scan.js +48 -10
  14. package/dist/durable/seat-proxy.d.ts +12 -1
  15. package/dist/durable/seat-proxy.js +55 -9
  16. package/dist/durable/seat-socket.d.ts +20 -2
  17. package/dist/durable/seat-socket.js +58 -0
  18. package/dist/durable/turn-chain.d.ts +8 -0
  19. package/dist/durable/turn-chain.js +25 -4
  20. package/dist/host/client.d.ts +41 -0
  21. package/dist/host/client.js +7 -0
  22. package/dist/host/control.d.ts +7 -0
  23. package/dist/host/control.js +15 -0
  24. package/dist/host/daemon.d.ts +20 -0
  25. package/dist/host/daemon.js +74 -1
  26. package/dist/host/inbox-delivery.d.ts +174 -0
  27. package/dist/host/inbox-delivery.js +453 -0
  28. package/dist/host/run.d.ts +18 -0
  29. package/dist/host/run.js +61 -0
  30. package/dist/host/s1-names.d.ts +2 -0
  31. package/dist/host/s1-names.js +2 -0
  32. package/dist/host/tmux.d.ts +7 -0
  33. package/dist/host/tmux.js +19 -0
  34. package/dist/index.js +62 -3
  35. package/dist/lib/api.d.ts +5 -0
  36. package/dist/lib/api.js +16 -0
  37. package/dist/lib/config.d.ts +7 -0
  38. package/dist/lib/config.js +21 -0
  39. package/dist/lib/durable/api.d.ts +11 -0
  40. package/dist/lib/durable/api.js +28 -3
  41. package/dist/lib/durable/gate.d.ts +1 -1
  42. package/dist/lib/durable/gate.js +8 -1
  43. package/dist/lib/durable/live.d.ts +5 -0
  44. package/dist/lib/durable/live.js +19 -0
  45. package/dist/lib/experimental.js +3 -0
  46. package/dist/lib/goal-api.d.ts +11 -4
  47. package/dist/lib/goal-api.js +4 -3
  48. package/dist/lib/launcher-config.d.ts +5 -0
  49. package/dist/lib/launcher-config.js +17 -2
  50. package/dist/lib/prompt.d.ts +4 -2
  51. package/dist/lib/prompt.js +15 -5
  52. package/dist/lib/runner/job.d.ts +2 -2
  53. package/dist/lib/runner/job.js +16 -3
  54. package/dist/lib/runner/pr-body.d.ts +17 -0
  55. package/dist/lib/runner/pr-body.js +42 -0
  56. package/dist/lib/runner/proxy.d.ts +18 -3
  57. package/dist/lib/runner/proxy.js +24 -5
  58. package/dist/lib/scan/pr-source.js +2 -0
  59. package/dist/lib/scan/record.d.ts +60 -9
  60. package/dist/lib/scan/record.js +157 -39
  61. package/dist/lib/scan/repo-picker.d.ts +32 -0
  62. package/dist/lib/scan/repo-picker.js +80 -0
  63. package/dist/lib/scan/report.d.ts +6 -8
  64. package/dist/lib/scan/report.js +21 -7
  65. package/dist/lib/scan/share.d.ts +5 -1
  66. package/dist/lib/scan/share.js +15 -5
  67. package/dist/lib/slack-durable.d.ts +123 -0
  68. package/dist/lib/slack-durable.js +237 -0
  69. package/dist/lib/slack-router.d.ts +7 -0
  70. package/dist/lib/slack-router.js +50 -4
  71. package/dist/vendor/durable-contract/index.js +992 -34
  72. package/dist/vendor/shared/durable-contract/admission.d.ts +81 -2
  73. package/dist/vendor/shared/durable-contract/admission.js +319 -2
  74. package/dist/vendor/shared/durable-contract/cli-surface.js +21 -0
  75. package/dist/vendor/shared/durable-contract/codes.d.ts +60 -0
  76. package/dist/vendor/shared/durable-contract/codes.js +22 -0
  77. package/dist/vendor/shared/durable-contract/events.d.ts +4 -2
  78. package/dist/vendor/shared/durable-contract/events.js +55 -1
  79. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +14 -0
  80. package/dist/vendor/shared/durable-contract/inbox.d.ts +299 -0
  81. package/dist/vendor/shared/durable-contract/inbox.js +462 -0
  82. package/dist/vendor/shared/durable-contract/index.d.ts +2 -0
  83. package/dist/vendor/shared/durable-contract/index.js +2 -0
  84. package/dist/vendor/shared/durable-contract/question.d.ts +218 -0
  85. package/dist/vendor/shared/durable-contract/question.js +373 -0
  86. package/dist/vendor/shared/durable-contract/routes.d.ts +44 -2
  87. package/dist/vendor/shared/durable-contract/routes.js +43 -1
  88. package/dist/vendor/shared/durable-contract/turn.d.ts +15 -1
  89. package/dist/vendor/shared/durable-contract/turn.js +33 -1
  90. package/dist/vendor/shared/durable-contract/vectors.d.ts +45 -0
  91. package/dist/vendor/shared/durable-contract/vectors.js +49 -0
  92. package/node_modules/@mnemom/aip-verifier/LICENSE +190 -0
  93. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +478 -0
  94. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +478 -0
  95. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +971 -0
  96. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +1 -0
  97. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +330 -0
  98. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +330 -0
  99. package/node_modules/@mnemom/aip-verifier/dist/index.js +933 -0
  100. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +1 -0
  101. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +879 -0
  102. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +1 -0
  103. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +96 -0
  104. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +96 -0
  105. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +875 -0
  106. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +1 -0
  107. package/node_modules/@mnemom/aip-verifier/package.json +86 -0
  108. package/node_modules/@noble/ed25519/LICENSE +21 -0
  109. package/node_modules/@noble/ed25519/README.md +371 -0
  110. package/node_modules/@noble/ed25519/index.d.ts +391 -0
  111. package/node_modules/@noble/ed25519/index.js +933 -0
  112. package/node_modules/@noble/ed25519/index.ts +1134 -0
  113. package/node_modules/@noble/ed25519/package.json +54 -0
  114. package/node_modules/@noble/hashes/LICENSE +21 -0
  115. package/node_modules/@noble/hashes/README.md +478 -0
  116. package/node_modules/@noble/hashes/_blake.d.ts +20 -0
  117. package/node_modules/@noble/hashes/_blake.js +59 -0
  118. package/node_modules/@noble/hashes/_md.d.ts +95 -0
  119. package/node_modules/@noble/hashes/_md.js +209 -0
  120. package/node_modules/@noble/hashes/_u64.d.ts +36 -0
  121. package/node_modules/@noble/hashes/_u64.js +77 -0
  122. package/node_modules/@noble/hashes/argon2.d.ts +148 -0
  123. package/node_modules/@noble/hashes/argon2.js +577 -0
  124. package/node_modules/@noble/hashes/blake1.d.ts +162 -0
  125. package/node_modules/@noble/hashes/blake1.js +630 -0
  126. package/node_modules/@noble/hashes/blake2.d.ts +203 -0
  127. package/node_modules/@noble/hashes/blake2.js +573 -0
  128. package/node_modules/@noble/hashes/blake3.d.ts +68 -0
  129. package/node_modules/@noble/hashes/blake3.js +310 -0
  130. package/node_modules/@noble/hashes/eskdf.d.ts +91 -0
  131. package/node_modules/@noble/hashes/eskdf.js +225 -0
  132. package/node_modules/@noble/hashes/hkdf.d.ts +70 -0
  133. package/node_modules/@noble/hashes/hkdf.js +152 -0
  134. package/node_modules/@noble/hashes/hmac.d.ts +50 -0
  135. package/node_modules/@noble/hashes/hmac.js +93 -0
  136. package/node_modules/@noble/hashes/index.d.ts +1 -0
  137. package/node_modules/@noble/hashes/index.js +34 -0
  138. package/node_modules/@noble/hashes/legacy.d.ts +107 -0
  139. package/node_modules/@noble/hashes/legacy.js +348 -0
  140. package/node_modules/@noble/hashes/package.json +91 -0
  141. package/node_modules/@noble/hashes/pbkdf2.d.ts +66 -0
  142. package/node_modules/@noble/hashes/pbkdf2.js +176 -0
  143. package/node_modules/@noble/hashes/scrypt.d.ts +98 -0
  144. package/node_modules/@noble/hashes/scrypt.js +304 -0
  145. package/node_modules/@noble/hashes/sha2.d.ts +176 -0
  146. package/node_modules/@noble/hashes/sha2.js +446 -0
  147. package/node_modules/@noble/hashes/sha3-addons.d.ts +533 -0
  148. package/node_modules/@noble/hashes/sha3-addons.js +973 -0
  149. package/node_modules/@noble/hashes/sha3.d.ts +208 -0
  150. package/node_modules/@noble/hashes/sha3.js +475 -0
  151. package/node_modules/@noble/hashes/src/_blake.ts +67 -0
  152. package/node_modules/@noble/hashes/src/_md.ts +227 -0
  153. package/node_modules/@noble/hashes/src/_u64.ts +111 -0
  154. package/node_modules/@noble/hashes/src/argon2.ts +693 -0
  155. package/node_modules/@noble/hashes/src/blake1.ts +677 -0
  156. package/node_modules/@noble/hashes/src/blake2.ts +648 -0
  157. package/node_modules/@noble/hashes/src/blake3.ts +344 -0
  158. package/node_modules/@noble/hashes/src/eskdf.ts +265 -0
  159. package/node_modules/@noble/hashes/src/hkdf.ts +161 -0
  160. package/node_modules/@noble/hashes/src/hmac.ts +130 -0
  161. package/node_modules/@noble/hashes/src/index.ts +33 -0
  162. package/node_modules/@noble/hashes/src/legacy.ts +361 -0
  163. package/node_modules/@noble/hashes/src/pbkdf2.ts +219 -0
  164. package/node_modules/@noble/hashes/src/scrypt.ts +375 -0
  165. package/node_modules/@noble/hashes/src/sha2.ts +486 -0
  166. package/node_modules/@noble/hashes/src/sha3-addons.ts +1157 -0
  167. package/node_modules/@noble/hashes/src/sha3.ts +525 -0
  168. package/node_modules/@noble/hashes/src/utils.ts +978 -0
  169. package/node_modules/@noble/hashes/src/webcrypto.ts +283 -0
  170. package/node_modules/@noble/hashes/utils.d.ts +560 -0
  171. package/node_modules/@noble/hashes/utils.js +687 -0
  172. package/node_modules/@noble/hashes/webcrypto.d.ts +121 -0
  173. package/node_modules/@noble/hashes/webcrypto.js +228 -0
  174. package/package.json +5 -2
  175. package/plugins/integrity-manifest.json +6 -2
  176. package/plugins/mnemom/hooks/hooks.json +12 -0
  177. package/plugins/mnemom/hooks/seat-ask.mjs +221 -0
@@ -0,0 +1,462 @@
1
+ // GENERATED from shared/durable-contract/src/inbox.ts by cli/scripts/sync-durable-contract.mjs; do not edit.
2
+ /**
3
+ * The durable inbox (S1 amendment 60, signed off by Wassim 2026-11-02): how work reaches a locked session
4
+ * without anyone typing into its seat. The owner, or an agent on the owner's allowlist for the
5
+ * recipient agent, sends a message to the session (ROUTES.own_inbox_send); the api stores it
6
+ * (encrypted, as a charter contract is) and journals inbox.admitted. The session's executor takes it
7
+ * (ROUTES.exec_inbox_take: the api marks it delivered at the live epoch and journals
8
+ * inbox.delivered), pastes the delivery text into the seat, and acks it when the turn that answered
9
+ * it ends (ROUTES.exec_inbox_ack: inbox.acked). Between take and ack the message's sha is in the
10
+ * resolver's `inbox_open`, which is what lets amendment 60 (e) admit the seat's `inbox=<sha>` turn
11
+ * unattended.
12
+ *
13
+ * Every inbox.* kind stays api-written (writerOf): delivered and acked are written by the api inside
14
+ * the take and ack routes, in the transaction that moves the inbox row, never through exec_append.
15
+ * So an executor cannot journal a delivery the api did not make, and the gateway can never see a
16
+ * turn whose sha is not yet open.
17
+ *
18
+ * This file holds the limits, the message sha and delivery text recipes, and the closed wire shapes
19
+ * of the inbox routes (routes.ts lists the routes). Nothing here carries message text into the
20
+ * journal: refs are ids, shas and enums only.
21
+ */
22
+ import { fieldOk, INBOX_OPEN_MAX } from "./admission.js";
23
+ import { canonicalJson, HEX64_RE, sha256Hex, utf8, UUID_RE } from "./canonical.js";
24
+ // ---- limits ---------------------------------------------------------------------------------
25
+ /** A message body is 1..this many UTF-8 bytes. */
26
+ export const INBOX_BODY_MAX_BYTES = 16384;
27
+ /** An admitted or delivered message nobody acks is dead (reason expired) this long after it was sent. */
28
+ export const INBOX_TTL_S = 7 * 24 * 60 * 60;
29
+ /** A message taken this many times without an ack is dead (reason delivery_limit) instead of taken again. */
30
+ export const INBOX_DELIVERY_LIMIT = 5;
31
+ /** exec_inbox_take returns at most this many messages; `limit` defaults to 1. */
32
+ export const INBOX_TAKE_LIMIT_MAX = 8;
33
+ /** own_inbox_list returns at most this many messages, newest first. */
34
+ export const INBOX_LIST_LIMIT = 100;
35
+ /** At most this many live (unrevoked) allowlisted senders per recipient agent. */
36
+ export const INBOX_SENDERS_MAX = 32;
37
+ /** The open-message cap: a send that would open more than this is refused (inbox_full). */
38
+ export const INBOX_OPEN_CAP = INBOX_OPEN_MAX;
39
+ /**
40
+ * Take re-offers a message delivered at the LIVE epoch once its last delivery is this old and it was
41
+ * neither acked nor released (the executor lost track of it: it died between the take and its own
42
+ * record, or gave up on a paste). The executor takes only while it tracks nothing for the seat and
43
+ * the seat is idle, so a re-offer never lands inside the turn answering it. At-least-once.
44
+ */
45
+ export const INBOX_REOFFER_S = 15 * 60;
46
+ /** idempotency_key: 1..200 of letters, digits and `_ . : -` (a Slack key is `slack:<channel>:<ts>`). */
47
+ export const INBOX_IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9_.:-]{1,200}$/;
48
+ /** An agents.id as the inbox takes it: opaque text, letters, digits, `_` and `-`, at most 64. */
49
+ export const INBOX_AGENT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
50
+ /** A sender label: one line of printable ASCII without `[` or `]`, 1..128 characters. */
51
+ export const INBOX_SENDER_LABEL_RE = /^[ -Z\\^-~]{1,128}$/;
52
+ // ---- vocabularies ---------------------------------------------------------------------------
53
+ /** durable_inbox.status. admitted and delivered are open; acked and dead are final. */
54
+ export const INBOX_STATUSES = ["admitted", "delivered", "acked", "dead"];
55
+ /** Who sent a message: the session's owner, or an agent on the owner's allowlist. */
56
+ export const INBOX_SENDER_KINDS = ["owner", "agent"];
57
+ /** inbox.rejected refs.reason: why a send by an authenticated sender that can see the session was refused. */
58
+ export const INBOX_REJECT_REASONS = [
59
+ "not_allowlisted",
60
+ "not_locked",
61
+ "ended",
62
+ "unsealed",
63
+ "inbox_full",
64
+ "body_too_large",
65
+ "body_invalid",
66
+ "unattended_off",
67
+ ];
68
+ /** inbox.dead refs.reason and durable_inbox.dead_reason. */
69
+ export const INBOX_DEAD_REASONS = ["expired", "delivery_limit", "session_ended"];
70
+ /**
71
+ * exec_inbox_release body.reason and inbox.reverted refs.reason: why the executor hands a delivered
72
+ * message back undone, instead of acking it.
73
+ * - unconfirmed: pasted, never reached the transcript;
74
+ * - refused: the turn it started ended on a gateway refusal (an API-error turn): no work was done;
75
+ * - undeliverable: the executor will not paste the text.
76
+ */
77
+ export const INBOX_RELEASE_REASONS = ["unconfirmed", "refused", "undeliverable"];
78
+ /**
79
+ * The `reason` of a 422 durable_inbox_sender_invalid: `sender` / `on_behalf_of` (a send whose
80
+ * sender identity is inconsistent), `agent_id` / `label` (a malformed allowlist change), `self` (the
81
+ * recipient itself), `not_found`, `other_org`, `not_owned` (not one of the owner's agents in the
82
+ * session's org), `too_many` (INBOX_SENDERS_MAX live senders already).
83
+ */
84
+ export const INBOX_SENDER_INVALID_REASONS = [
85
+ "sender",
86
+ "on_behalf_of",
87
+ "agent_id",
88
+ "label",
89
+ "self",
90
+ "not_found",
91
+ "other_org",
92
+ "not_owned",
93
+ "too_many",
94
+ ];
95
+ /**
96
+ * The refusal code and HTTP status of each INBOX_REJECT_REASONS value (codes.ts DURABLE_CODES). Each
97
+ * refused send by a sender that may see the session journals inbox.rejected {reason, sender_kind};
98
+ * a caller who cannot see it is refused before anything is read or written.
99
+ */
100
+ export const INBOX_REJECT_CODES = Object.freeze({
101
+ not_allowlisted: { code: "durable_inbox_not_allowlisted", status: 403 },
102
+ not_locked: { code: "durable_inbox_closed", status: 409 },
103
+ ended: { code: "durable_inbox_closed", status: 409 },
104
+ unsealed: { code: "durable_inbox_closed", status: 409 },
105
+ inbox_full: { code: "durable_inbox_full", status: 429 },
106
+ body_too_large: { code: "durable_inbox_body_invalid", status: 422 },
107
+ body_invalid: { code: "durable_inbox_body_invalid", status: 422 },
108
+ unattended_off: { code: "durable_inbox_closed", status: 409 },
109
+ });
110
+ /** The kinds amendment 60 gives an emitter (they stay in the frozen NO_M1_EMITTER_KINDS M1 baseline). */
111
+ export const A60_EMITTED_KINDS = [
112
+ "inbox.admitted",
113
+ "inbox.rejected",
114
+ "inbox.coalesced",
115
+ "inbox.delivered",
116
+ "inbox.acked",
117
+ "inbox.reverted",
118
+ "inbox.dead",
119
+ ];
120
+ /** The sha preimage: canonicalJson of the identity plus `v: 1`. */
121
+ export function inboxShaPreimage(m) {
122
+ return canonicalJson({
123
+ v: 1,
124
+ session_id: m.session_id,
125
+ sender_kind: m.sender_kind,
126
+ sender_agent_id: m.sender_agent_id ?? null,
127
+ on_behalf_of: m.on_behalf_of ?? null,
128
+ in_reply_to: m.in_reply_to ?? null,
129
+ body: m.body,
130
+ });
131
+ }
132
+ /**
133
+ * A message's sha: sha256 of inboxShaPreimage, lowercase hex. The api computes it at send. The same
134
+ * sender sending the same body to the same session while an earlier copy is still open gets that
135
+ * copy back (coalesced); it is the value the seat stamps as `inbox=<sha>` and the resolver lists in
136
+ * `inbox_open`.
137
+ */
138
+ export async function inboxSha(m) {
139
+ return sha256Hex(inboxShaPreimage(m));
140
+ }
141
+ /**
142
+ * A body may carry TAB, LF and CR, and no other control character: no C0 (NUL, ESC, ...), DEL or C1.
143
+ * The daemon pastes a delivery into a terminal, where ESC[201~ would end the bracketed paste and the
144
+ * bytes after it would be typed as keys.
145
+ */
146
+ // eslint-disable-next-line no-control-regex
147
+ const INBOX_BODY_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
148
+ /** A UTF-8 body within the limits: 1..INBOX_BODY_MAX_BYTES bytes, no control character but TAB, LF, CR. */
149
+ export function inboxBodyOk(body) {
150
+ if (typeof body !== "string" || body.length === 0 || INBOX_BODY_CONTROL_RE.test(body))
151
+ return false;
152
+ return utf8(body).length <= INBOX_BODY_MAX_BYTES;
153
+ }
154
+ /** The first line of every delivery text: `[mnemom inbox <sha>]`, then who sent it. */
155
+ export const INBOX_MARKER_RE = /^\[mnemom inbox ([0-9a-f]{64})\]/;
156
+ /**
157
+ * The exact text the seat is given (the api renders it in exec_inbox_take; the daemon pastes it
158
+ * verbatim). Line 1 is the marker and the sender, line 2 is empty, the body follows unchanged:
159
+ * `[mnemom inbox <sha>] from the owner`
160
+ * `[mnemom inbox <sha>] from agent <label> on behalf of the owner`
161
+ * and ` (answer to question <approval_id>)` when the message answers one. The marker is read from
162
+ * line 1 only (inboxMarkerOf), so a body cannot pose as another message.
163
+ */
164
+ export function inboxDeliveryText(m) {
165
+ if (!HEX64_RE.test(m.sha))
166
+ throw new RangeError("inbox sha must be 64 lowercase hex");
167
+ let who = "the owner";
168
+ if (m.sender_kind === "agent") {
169
+ const label = m.sender_label ?? "";
170
+ if (!INBOX_SENDER_LABEL_RE.test(label))
171
+ throw new RangeError("inbox sender label invalid");
172
+ who = `agent ${label} on behalf of the owner`;
173
+ }
174
+ if (m.in_reply_to !== null && !UUID_RE.test(m.in_reply_to))
175
+ throw new RangeError("in_reply_to must be a lowercase uuid");
176
+ const reply = m.in_reply_to === null ? "" : ` (answer to question ${m.in_reply_to})`;
177
+ return `[mnemom inbox ${m.sha}] from ${who}${reply}\n\n${m.body}`;
178
+ }
179
+ /**
180
+ * The sha a prompt carries, read from its first line only, or null. The seat proxy stamps
181
+ * `inbox=<sha>` on the turn entry whose text this returns non-null for. Restrict-only: a stamp only
182
+ * makes a turn unattended, and the gateway admits it only while the sha is in `inbox_open`.
183
+ */
184
+ export function inboxMarkerOf(text) {
185
+ const nl = text.indexOf("\n");
186
+ return INBOX_MARKER_RE.exec(nl < 0 ? text : text.slice(0, nl))?.[1] ?? null;
187
+ }
188
+ // ---- wire shapes ----------------------------------------------------------------------------
189
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
190
+ const keysExactly = (o, keys) => Object.keys(o).length === keys.length && keys.every((k) => k in o);
191
+ const keysWithin = (o, required, optional) => required.every((k) => k in o) &&
192
+ Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
193
+ const str = (v, re) => typeof v === "string" && re.test(v);
194
+ const uuid = (v, nullable = false) => fieldOk({ type: "uuid", nullable }, v);
195
+ const ts = (v, nullable = false) => fieldOk({ type: "timestamp", nullable }, v);
196
+ const int = (v, min = 0) => Number.isSafeInteger(v) && v >= min;
197
+ const oneOf = (v, values) => typeof v === "string" && values.includes(v);
198
+ const agentId = (v) => str(v, INBOX_AGENT_ID_RE);
199
+ const nonEmpty = (v) => typeof v === "string" && v.length > 0;
200
+ /** POST own_inbox_send body: exactly {text, idempotency_key}. Sender and on_behalf_of come from the credential and the allowlist, never the body. */
201
+ export const INBOX_SEND_BODY_KEYS = ["text", "idempotency_key"];
202
+ /** A send body: exactly INBOX_SEND_BODY_KEYS, text passing inboxBodyOk, idempotency_key INBOX_IDEMPOTENCY_KEY_RE. */
203
+ export function inboxSendBodyOk(v) {
204
+ return (isObj(v) &&
205
+ keysExactly(v, INBOX_SEND_BODY_KEYS) &&
206
+ inboxBodyOk(v.text) &&
207
+ str(v.idempotency_key, INBOX_IDEMPOTENCY_KEY_RE));
208
+ }
209
+ /**
210
+ * The 200 answer of own_inbox_send, exactly these keys:
211
+ * - inbox_id, sha: the message (a coalesced or deduped send names the earlier, still-open copy);
212
+ * - status: its INBOX_STATUSES value now;
213
+ * - coalesced: an open message with this sha already existed, nothing new was stored;
214
+ * - deduped: this idempotency_key was sent before, this is that send's message;
215
+ * - sender_kind, sender_agent_id, on_behalf_of: as stored (the owner's user id for an agent send);
216
+ * - expires_at: when an unacked message goes dead.
217
+ * A refusal is a durable error (INBOX_REJECT_CODES), never a 200.
218
+ */
219
+ export const INBOX_SEND_RESPONSE = Object.freeze({
220
+ inbox_id: "uuid",
221
+ sha: "hex64",
222
+ status: "inbox_status",
223
+ coalesced: "boolean",
224
+ deduped: "boolean",
225
+ sender_kind: "inbox_sender_kind",
226
+ sender_agent_id: "agent_id|null",
227
+ on_behalf_of: "string|null",
228
+ expires_at: "timestamp",
229
+ });
230
+ const senderOk = (v) => oneOf(v.sender_kind, INBOX_SENDER_KINDS) &&
231
+ (v.sender_kind === "owner"
232
+ ? v.sender_agent_id === null && v.on_behalf_of === null
233
+ : agentId(v.sender_agent_id) && nonEmpty(v.on_behalf_of));
234
+ export function inboxSendResponseOk(v) {
235
+ return (isObj(v) &&
236
+ keysExactly(v, Object.keys(INBOX_SEND_RESPONSE)) &&
237
+ uuid(v.inbox_id) &&
238
+ str(v.sha, HEX64_RE) &&
239
+ oneOf(v.status, INBOX_STATUSES) &&
240
+ typeof v.coalesced === "boolean" &&
241
+ typeof v.deduped === "boolean" &&
242
+ senderOk(v) &&
243
+ ts(v.expires_at));
244
+ }
245
+ /**
246
+ * One own_inbox_list item, exactly these keys; content-free (no body). GET own_inbox_list answers
247
+ * `{messages: [item, ...]}`, newest first, at most INBOX_LIST_LIMIT.
248
+ */
249
+ export const INBOX_LIST_ITEM = Object.freeze({
250
+ inbox_id: "uuid",
251
+ sha: "hex64",
252
+ status: "inbox_status",
253
+ sender_kind: "inbox_sender_kind",
254
+ sender_agent_id: "agent_id|null",
255
+ on_behalf_of: "string|null",
256
+ in_reply_to: "uuid|null",
257
+ body_bytes: "integer>=1",
258
+ delivery_count: "integer",
259
+ dead_reason: "inbox_dead_reason|null",
260
+ created_at: "timestamp",
261
+ delivered_at: "timestamp|null",
262
+ acked_at: "timestamp|null",
263
+ expires_at: "timestamp",
264
+ });
265
+ export function inboxListItemOk(v) {
266
+ return (isObj(v) &&
267
+ keysExactly(v, Object.keys(INBOX_LIST_ITEM)) &&
268
+ uuid(v.inbox_id) &&
269
+ str(v.sha, HEX64_RE) &&
270
+ oneOf(v.status, INBOX_STATUSES) &&
271
+ senderOk(v) &&
272
+ uuid(v.in_reply_to, true) &&
273
+ int(v.body_bytes, 1) &&
274
+ v.body_bytes <= INBOX_BODY_MAX_BYTES &&
275
+ int(v.delivery_count) &&
276
+ (v.status === "dead" ? oneOf(v.dead_reason, INBOX_DEAD_REASONS) : v.dead_reason === null) &&
277
+ ts(v.created_at) &&
278
+ ts(v.delivered_at, true) &&
279
+ ts(v.acked_at, true) &&
280
+ ts(v.expires_at));
281
+ }
282
+ export function inboxListResponseOk(v) {
283
+ return (isObj(v) &&
284
+ keysExactly(v, ["messages"]) &&
285
+ Array.isArray(v.messages) &&
286
+ v.messages.length <= INBOX_LIST_LIMIT &&
287
+ v.messages.every(inboxListItemOk));
288
+ }
289
+ /** POST own_inbox_sender_add body: agent_id required, label optional. */
290
+ export const INBOX_SENDER_ADD_BODY_KEYS = ["agent_id", "label"];
291
+ export function inboxSenderAddBodyOk(v) {
292
+ return (isObj(v) &&
293
+ keysWithin(v, ["agent_id"], ["label"]) &&
294
+ str(v.agent_id, INBOX_AGENT_ID_RE) &&
295
+ (!("label" in v) || str(v.label, INBOX_SENDER_LABEL_RE)));
296
+ }
297
+ /** DELETE own_inbox_sender_remove body: exactly {agent_id}. */
298
+ export const INBOX_SENDER_REMOVE_BODY_KEYS = ["agent_id"];
299
+ export function inboxSenderRemoveBodyOk(v) {
300
+ return isObj(v) && keysExactly(v, INBOX_SENDER_REMOVE_BODY_KEYS) && str(v.agent_id, INBOX_AGENT_ID_RE);
301
+ }
302
+ /**
303
+ * One allowlisted sender, exactly these keys. The allowlist belongs to the recipient AGENT (it
304
+ * outlives any one session); the routes reach it through a session the owner owns. added_by is the
305
+ * owner's user id (null only after a DSR erase).
306
+ */
307
+ export const INBOX_SENDER_ITEM = Object.freeze({
308
+ sender_agent_id: "agent_id",
309
+ label: "label|null",
310
+ added_by: "string|null",
311
+ created_at: "timestamp",
312
+ });
313
+ export function inboxSenderItemOk(v) {
314
+ return (isObj(v) &&
315
+ keysExactly(v, Object.keys(INBOX_SENDER_ITEM)) &&
316
+ agentId(v.sender_agent_id) &&
317
+ (v.label === null || str(v.label, INBOX_SENDER_LABEL_RE)) &&
318
+ (v.added_by === null || nonEmpty(v.added_by)) &&
319
+ ts(v.created_at));
320
+ }
321
+ /** GET own_inbox_senders: exactly {recipient_agent_id, senders}, the live (unrevoked) rows. */
322
+ export function inboxSendersResponseOk(v) {
323
+ return (isObj(v) &&
324
+ keysExactly(v, ["recipient_agent_id", "senders"]) &&
325
+ agentId(v.recipient_agent_id) &&
326
+ Array.isArray(v.senders) &&
327
+ v.senders.length <= INBOX_SENDERS_MAX &&
328
+ v.senders.every(inboxSenderItemOk));
329
+ }
330
+ /**
331
+ * POST own_inbox_sender_add / DELETE own_inbox_sender_remove answer: exactly
332
+ * {recipient_agent_id, sender_agent_id, changed}; changed false when the row was already live
333
+ * (add) or already absent or revoked (remove).
334
+ */
335
+ export function inboxSenderChangeResponseOk(v) {
336
+ return (isObj(v) &&
337
+ keysExactly(v, ["recipient_agent_id", "sender_agent_id", "changed"]) &&
338
+ agentId(v.recipient_agent_id) &&
339
+ agentId(v.sender_agent_id) &&
340
+ v.recipient_agent_id !== v.sender_agent_id &&
341
+ typeof v.changed === "boolean");
342
+ }
343
+ /**
344
+ * GET exec_inbox: a content-free summary for a session the executor is home to or holds, exactly
345
+ * {admitted, delivered, oldest_admitted_at}. The daemon reads it to decide whether a parked homed
346
+ * session has work to claim; it marks nothing delivered.
347
+ */
348
+ export function inboxSummaryResponseOk(v) {
349
+ return (isObj(v) &&
350
+ keysExactly(v, ["admitted", "delivered", "oldest_admitted_at"]) &&
351
+ int(v.admitted) &&
352
+ int(v.delivered) &&
353
+ (v.admitted === 0 ? v.oldest_admitted_at === null : ts(v.oldest_admitted_at)));
354
+ }
355
+ /** POST exec_inbox_take body: epoch and nonce (the live lease's) required, limit optional. */
356
+ export const INBOX_TAKE_BODY_KEYS = ["epoch", "nonce", "limit"];
357
+ export function inboxTakeBodyOk(v) {
358
+ return (isObj(v) &&
359
+ keysWithin(v, ["epoch", "nonce"], ["limit"]) &&
360
+ int(v.epoch) &&
361
+ nonEmpty(v.nonce) &&
362
+ (!("limit" in v) || (int(v.limit, 1) && v.limit <= INBOX_TAKE_LIMIT_MAX)));
363
+ }
364
+ /**
365
+ * One taken message, exactly these keys. `text` is inboxDeliveryText of the message, rendered by
366
+ * the api; the daemon pastes it verbatim. attempt is the message's delivery_count after this take.
367
+ */
368
+ export const INBOX_TAKE_ITEM = Object.freeze({
369
+ inbox_id: "uuid",
370
+ sha: "hex64",
371
+ text: "string",
372
+ attempt: "integer>=1",
373
+ sender_kind: "inbox_sender_kind",
374
+ sender_agent_id: "agent_id|null",
375
+ on_behalf_of: "string|null",
376
+ in_reply_to: "uuid|null",
377
+ created_at: "timestamp",
378
+ });
379
+ export function inboxTakeItemOk(v) {
380
+ return (isObj(v) &&
381
+ keysExactly(v, Object.keys(INBOX_TAKE_ITEM)) &&
382
+ uuid(v.inbox_id) &&
383
+ str(v.sha, HEX64_RE) &&
384
+ typeof v.text === "string" &&
385
+ inboxMarkerOf(v.text) === v.sha &&
386
+ int(v.attempt, 1) &&
387
+ v.attempt <= INBOX_DELIVERY_LIMIT &&
388
+ senderOk(v) &&
389
+ uuid(v.in_reply_to, true) &&
390
+ ts(v.created_at));
391
+ }
392
+ /** POST exec_inbox_take answer: exactly {messages}, oldest first, at most the body's limit. */
393
+ export function inboxTakeResponseOk(v) {
394
+ return (isObj(v) &&
395
+ keysExactly(v, ["messages"]) &&
396
+ Array.isArray(v.messages) &&
397
+ v.messages.length <= INBOX_TAKE_LIMIT_MAX &&
398
+ v.messages.every(inboxTakeItemOk));
399
+ }
400
+ /** POST exec_inbox_ack body: exactly {epoch, nonce, inbox_id, sha}. */
401
+ export const INBOX_ACK_BODY_KEYS = ["epoch", "nonce", "inbox_id", "sha"];
402
+ export function inboxAckBodyOk(v) {
403
+ return (isObj(v) &&
404
+ keysExactly(v, INBOX_ACK_BODY_KEYS) &&
405
+ int(v.epoch) &&
406
+ nonEmpty(v.nonce) &&
407
+ uuid(v.inbox_id) &&
408
+ str(v.sha, HEX64_RE));
409
+ }
410
+ /**
411
+ * POST exec_inbox_ack answer: exactly {inbox_id, sha, status: "acked", deduped}. A message that is
412
+ * not delivered to this session at this epoch with this sha is 409 durable_inbox_not_open.
413
+ */
414
+ export function inboxAckResponseOk(v) {
415
+ return (isObj(v) &&
416
+ keysExactly(v, ["inbox_id", "sha", "status", "deduped"]) &&
417
+ uuid(v.inbox_id) &&
418
+ str(v.sha, HEX64_RE) &&
419
+ v.status === "acked" &&
420
+ typeof v.deduped === "boolean");
421
+ }
422
+ /** POST exec_inbox_release body: exactly {epoch, nonce, inbox_id, sha, reason}. */
423
+ export const INBOX_RELEASE_BODY_KEYS = ["epoch", "nonce", "inbox_id", "sha", "reason"];
424
+ export function inboxReleaseBodyOk(v) {
425
+ return (isObj(v) &&
426
+ keysExactly(v, INBOX_RELEASE_BODY_KEYS) &&
427
+ int(v.epoch) &&
428
+ nonEmpty(v.nonce) &&
429
+ uuid(v.inbox_id) &&
430
+ str(v.sha, HEX64_RE) &&
431
+ oneOf(v.reason, INBOX_RELEASE_REASONS));
432
+ }
433
+ /**
434
+ * POST exec_inbox_release answer: exactly {inbox_id, sha, status}. The api moves a message delivered
435
+ * to this session at this epoch back to `admitted` and journals inbox.reverted {inbox_id, sha,
436
+ * reason}, so the next take offers it again; when its delivery_count already reached
437
+ * INBOX_DELIVERY_LIMIT it goes `dead` instead (inbox.dead delivery_limit). Either way its sha leaves
438
+ * `inbox_open` in the same transaction. Anything else is 409 durable_inbox_not_open.
439
+ */
440
+ export function inboxReleaseResponseOk(v) {
441
+ return (isObj(v) &&
442
+ keysExactly(v, ["inbox_id", "sha", "status"]) &&
443
+ uuid(v.inbox_id) &&
444
+ str(v.sha, HEX64_RE) &&
445
+ (v.status === "admitted" || v.status === "dead"));
446
+ }
447
+ /** The journal idempotency keys the api uses for inbox events (one event per fact). */
448
+ export const inboxIdempotencyKeys = {
449
+ admitted: (inboxId) => `inbox:${inboxId}:admitted`,
450
+ coalesced: (inboxId, idempotencyKey) => `inbox:${inboxId}:coalesced:${idempotencyKey}`,
451
+ /**
452
+ * A refused send: keyed on the caller's idempotency_key when it is well-formed, else on the api's
453
+ * own request id (a body_invalid refusal may carry no usable key).
454
+ */
455
+ rejected: (sessionId, idempotencyKey, requestId) => typeof idempotencyKey === "string" && INBOX_IDEMPOTENCY_KEY_RE.test(idempotencyKey)
456
+ ? `inbox:${sessionId}:rejected:${idempotencyKey}`
457
+ : `inbox:${sessionId}:rejected:request:${requestId}`,
458
+ delivered: (inboxId, epoch, attempt) => `inbox:${inboxId}:delivered:${epoch}:${attempt}`,
459
+ acked: (inboxId) => `inbox:${inboxId}:acked`,
460
+ reverted: (inboxId, epoch, attempt) => `inbox:${inboxId}:reverted:${epoch}:${attempt}`,
461
+ dead: (inboxId) => `inbox:${inboxId}:dead`,
462
+ };
@@ -27,6 +27,8 @@ export * from "./seal.js";
27
27
  export * from "./paths.js";
28
28
  export * from "./executor-state.js";
29
29
  export * from "./routes.js";
30
+ export * from "./inbox.js";
31
+ export * from "./question.js";
30
32
  export * from "./doors.js";
31
33
  export * from "./journal-approvals.js";
32
34
  export * from "./secret-scope.js";
@@ -28,6 +28,8 @@ export * from "./seal.js";
28
28
  export * from "./paths.js";
29
29
  export * from "./executor-state.js";
30
30
  export * from "./routes.js";
31
+ export * from "./inbox.js";
32
+ export * from "./question.js";
31
33
  export * from "./doors.js";
32
34
  export * from "./journal-approvals.js";
33
35
  export * from "./secret-scope.js";