@mnemom/mnemom 0.17.34 → 0.17.35
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.
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent-goal.d.ts +8 -3
- package/dist/commands/agent-goal.js +22 -8
- package/dist/commands/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +19 -6
- package/dist/commands/agent-slack.js +32 -0
- package/dist/commands/durable-inbox.d.ts +51 -0
- package/dist/commands/durable-inbox.js +134 -0
- package/dist/commands/durable-questions.d.ts +64 -0
- package/dist/commands/durable-questions.js +161 -0
- package/dist/durable/seat-proxy.d.ts +12 -1
- package/dist/durable/seat-proxy.js +55 -9
- package/dist/durable/seat-socket.d.ts +20 -2
- package/dist/durable/seat-socket.js +58 -0
- package/dist/durable/turn-chain.d.ts +8 -0
- package/dist/durable/turn-chain.js +25 -4
- package/dist/host/client.d.ts +41 -0
- package/dist/host/client.js +7 -0
- package/dist/host/control.d.ts +7 -0
- package/dist/host/control.js +15 -0
- package/dist/host/daemon.d.ts +20 -0
- package/dist/host/daemon.js +74 -1
- package/dist/host/inbox-delivery.d.ts +174 -0
- package/dist/host/inbox-delivery.js +453 -0
- package/dist/host/run.d.ts +18 -0
- package/dist/host/run.js +61 -0
- package/dist/host/s1-names.d.ts +2 -0
- package/dist/host/s1-names.js +2 -0
- package/dist/host/tmux.d.ts +7 -0
- package/dist/host/tmux.js +19 -0
- package/dist/index.js +59 -1
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/durable/api.d.ts +11 -0
- package/dist/lib/durable/api.js +28 -3
- package/dist/lib/durable/gate.d.ts +1 -1
- package/dist/lib/durable/gate.js +8 -1
- package/dist/lib/durable/live.d.ts +5 -0
- package/dist/lib/durable/live.js +19 -0
- package/dist/lib/experimental.js +3 -0
- package/dist/lib/goal-api.d.ts +11 -4
- package/dist/lib/goal-api.js +4 -3
- package/dist/lib/launcher-config.d.ts +5 -0
- package/dist/lib/launcher-config.js +17 -2
- package/dist/lib/prompt.d.ts +4 -2
- package/dist/lib/prompt.js +15 -5
- package/dist/lib/runner/job.d.ts +2 -2
- package/dist/lib/runner/job.js +16 -3
- package/dist/lib/runner/pr-body.d.ts +17 -0
- package/dist/lib/runner/pr-body.js +42 -0
- package/dist/lib/runner/proxy.d.ts +18 -3
- package/dist/lib/runner/proxy.js +24 -5
- package/dist/lib/scan/record.d.ts +60 -9
- package/dist/lib/scan/record.js +157 -39
- package/dist/lib/scan/report.d.ts +6 -8
- package/dist/lib/scan/report.js +21 -7
- package/dist/lib/scan/share.d.ts +5 -1
- package/dist/lib/scan/share.js +15 -5
- package/dist/lib/slack-durable.d.ts +123 -0
- package/dist/lib/slack-durable.js +237 -0
- package/dist/lib/slack-router.d.ts +7 -0
- package/dist/lib/slack-router.js +50 -4
- package/dist/vendor/durable-contract/index.js +992 -34
- package/dist/vendor/shared/durable-contract/admission.d.ts +81 -2
- package/dist/vendor/shared/durable-contract/admission.js +319 -2
- package/dist/vendor/shared/durable-contract/cli-surface.js +21 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +60 -0
- package/dist/vendor/shared/durable-contract/codes.js +22 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +4 -2
- package/dist/vendor/shared/durable-contract/events.js +55 -1
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +14 -0
- package/dist/vendor/shared/durable-contract/inbox.d.ts +299 -0
- package/dist/vendor/shared/durable-contract/inbox.js +462 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +2 -0
- package/dist/vendor/shared/durable-contract/index.js +2 -0
- package/dist/vendor/shared/durable-contract/question.d.ts +218 -0
- package/dist/vendor/shared/durable-contract/question.js +373 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +44 -2
- package/dist/vendor/shared/durable-contract/routes.js +43 -1
- package/dist/vendor/shared/durable-contract/turn.d.ts +15 -1
- package/dist/vendor/shared/durable-contract/turn.js +33 -1
- package/dist/vendor/shared/durable-contract/vectors.d.ts +45 -0
- package/dist/vendor/shared/durable-contract/vectors.js +49 -0
- package/package.json +1 -1
- package/plugins/integrity-manifest.json +6 -2
- package/plugins/mnemom/hooks/hooks.json +12 -0
- 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";
|