@hraness/message-like-me 0.8.4 → 0.8.6
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/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/agentic-messaging-v1.js +481 -25
- package/dist/cli.js +22925 -4839
- package/dist/ensoul-source-v1.js +4 -4
- package/dist/{cli-x1qncxm7.js → index-7dfvy41v.js} +4 -462
- package/dist/{cli-ththzwja.js → index-y1aec88m.js} +1 -5
- package/dist/index.js +4 -4
- package/dist/message-bundle-v1.js +580 -18
- package/dist/message-bundle-v2.js +639 -21
- package/docs/command-runtime.md +122 -0
- package/docs/effect-license.txt +21 -0
- package/package.json +6 -3
- package/dist/cli-bs3db5jr.js +0 -643
- package/dist/cli-d7qv38ab.js +0 -485
- package/dist/cli-ry4128kz.js +0 -584
- /package/dist/{cli-kw20gkk3.js → index-kw20gkk3.js} +0 -0
- /package/dist/{cli-qqafdvz9.js → index-qqafdvz9.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.6 (2026-09-07)
|
|
4
|
+
|
|
5
|
+
- Make the exact public `@hraness/message-like-me@0.8.6` npm package the
|
|
6
|
+
supported default install, with the same reviewed bytes mirrored in the
|
|
7
|
+
immutable GitHub Release.
|
|
8
|
+
- Give the paired Message Like Me and Ensoul skill installer one command-owned
|
|
9
|
+
lifecycle through copying, backup, publication and sequential recovery.
|
|
10
|
+
- Bound physical inventories and verify copied bytes before publication. Retain
|
|
11
|
+
unknown or substituted entries during cleanup, and preserve missing project
|
|
12
|
+
directory creation beneath a verified physical ancestor.
|
|
13
|
+
- Keep both installed skills after publication succeeds, even when backup cleanup
|
|
14
|
+
or output confirmation fails. Report incomplete cleanup with fixed diagnostics
|
|
15
|
+
that omit private data while preserving the original failure.
|
|
16
|
+
- Strengthen the development Effect checker with generator alias and wrapper
|
|
17
|
+
regressions. Public protocol graphs and local-only behavior remain unchanged.
|
|
18
|
+
|
|
19
|
+
## 0.8.5 (2026-09-06)
|
|
20
|
+
|
|
21
|
+
- Make the exact public `@hraness/message-like-me@0.8.5` npm package the
|
|
22
|
+
supported default install, with the same reviewed bytes mirrored in the
|
|
23
|
+
immutable GitHub Release.
|
|
24
|
+
- Give each command one scoped owner for its private store, source reads and
|
|
25
|
+
artifact publication. Typed failures retain the existing CLI diagnostics and
|
|
26
|
+
exit codes; store transactions remain synchronous.
|
|
27
|
+
- Publish private exports through exclusive staging and no-overwrite paths.
|
|
28
|
+
Reconcile receipt failures before cleanup, retain files whose ownership or
|
|
29
|
+
publication status cannot be proved, and bound cleanup reads even if a file
|
|
30
|
+
grows during inspection.
|
|
31
|
+
- Bundle Effect 3.22.1 inside the CLI. Public protocol entrypoints remain pure,
|
|
32
|
+
preserve their declarations, and require no new consumer dependency.
|
|
33
|
+
- Bound private JSON read allocation to the admitted file size, including
|
|
34
|
+
short reads and files that grow during inspection.
|
|
35
|
+
|
|
3
36
|
## 0.8.4 (2026-09-06)
|
|
4
37
|
|
|
5
38
|
- Make the exact public `@hraness/message-like-me@0.8.4` npm package the
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Message Like Me requires Bun 1.3.14 or newer. Install the exact public npm
|
|
|
43
43
|
package, then install both bundled Agent Skills:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
bun add --global @hraness/message-like-me@0.8.
|
|
46
|
+
bun add --global @hraness/message-like-me@0.8.6
|
|
47
47
|
messagelikeme skill install
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -1,30 +1,486 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
canonicalJson,
|
|
4
|
+
sha256
|
|
5
|
+
} from "./index-y1aec88m.js";
|
|
6
|
+
|
|
7
|
+
// src/agentic-messaging-v1.ts
|
|
8
|
+
import { types as nodeTypes } from "util";
|
|
9
|
+
var AGENTIC_MESSAGING_V1_SCHEMA_VERSION = 1;
|
|
10
|
+
var AGENT_MESSAGE_DRAFT_V1_FORMAT = "message-like-me.agent-message-draft";
|
|
11
|
+
var AGENT_MESSAGE_HANDOFF_REQUEST_V1_FORMAT = "message-like-me.agent-message-handoff-request";
|
|
12
|
+
var AGENT_MESSAGE_HANDOFF_V1_FORMAT = "message-like-me.agent-message-handoff";
|
|
13
|
+
var AGENT_MESSAGE_AUDIT_V1_FORMAT = "message-like-me.agent-message-handoff-audit";
|
|
14
|
+
var WRENCH_MESSAGING_CONTEXT_BINDING_V1_FORMAT = "wrench.messaging-context-binding";
|
|
15
|
+
var WRENCH_MESSAGING_RECEIPT_BINDING_V1_FORMAT = "wrench.messaging-receipt-binding";
|
|
16
|
+
var WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_ID = "wrench.messaging-context-binding.v1";
|
|
17
|
+
var WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_HASH = "5e64da6a3d826e7f6fa3db7dca0a4ba92c10cfb784981e71a25aed9513a5c687";
|
|
18
|
+
var WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_DESCRIPTOR = Object.freeze({
|
|
19
|
+
contractId: WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_ID,
|
|
20
|
+
fields: Object.freeze([
|
|
21
|
+
"schemaVersion:1",
|
|
22
|
+
"format:wrench.messaging-context-binding",
|
|
23
|
+
"contractId:wrench.messaging-context-binding.v1",
|
|
24
|
+
"contractHash:sha256",
|
|
25
|
+
"routeRef:opaque",
|
|
26
|
+
"contextRef:opaque",
|
|
27
|
+
"exactDataRevision:sha256",
|
|
28
|
+
"latestMessageRevision:sha256",
|
|
29
|
+
"validatedAt:rfc3339",
|
|
30
|
+
"expiresAt:rfc3339"
|
|
31
|
+
]),
|
|
32
|
+
format: "wrench.messaging-contract-descriptor",
|
|
33
|
+
schemaVersion: 1
|
|
34
|
+
});
|
|
35
|
+
var WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_ID = "wrench.messaging-receipt-binding.v1";
|
|
36
|
+
var WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_HASH = "7f6cf724f0200b2399e4f4641c637b20b48914fc5c9b13755127a8ec69fe66f4";
|
|
37
|
+
var WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_DESCRIPTOR = Object.freeze({
|
|
38
|
+
contractId: WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_ID,
|
|
39
|
+
fields: Object.freeze([
|
|
40
|
+
"schemaVersion:1",
|
|
41
|
+
"format:wrench.messaging-receipt-binding",
|
|
42
|
+
"contractId:wrench.messaging-receipt-binding.v1",
|
|
43
|
+
"contractHash:sha256",
|
|
44
|
+
"clientIntentSha256:sha256",
|
|
45
|
+
"routeRefSha256:sha256",
|
|
46
|
+
"contextRefSha256:sha256",
|
|
47
|
+
"turnDigest:sha256",
|
|
48
|
+
"previewDigest:sha256",
|
|
49
|
+
"runId:opaque",
|
|
50
|
+
"state:submitted|failed|partial|indeterminate",
|
|
51
|
+
"partCount:uint",
|
|
52
|
+
"provenPartCount:uint",
|
|
53
|
+
"receiptSha256:sha256",
|
|
54
|
+
"recordedAt:rfc3339"
|
|
55
|
+
]),
|
|
56
|
+
format: "wrench.messaging-contract-descriptor",
|
|
57
|
+
schemaVersion: 1
|
|
58
|
+
});
|
|
59
|
+
var AGENTIC_MESSAGING_V1_LIMITS = Object.freeze({
|
|
60
|
+
bubbles: 8,
|
|
61
|
+
bubbleBytes: 8 * 1024,
|
|
62
|
+
totalBubbleBytes: 32 * 1024,
|
|
63
|
+
identifierBytes: 1024,
|
|
64
|
+
privateJsonBytes: 128 * 1024,
|
|
65
|
+
handoffLifetimeMilliseconds: 10 * 60 * 1000,
|
|
66
|
+
maximumContextLifetimeMilliseconds: 24 * 60 * 60 * 1000
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
class AgenticMessagingV1ContractError extends TypeError {
|
|
70
|
+
code = "agentic-messaging-v1-contract";
|
|
71
|
+
constructor(message, options) {
|
|
72
|
+
super(message, options);
|
|
73
|
+
this.name = "AgenticMessagingV1ContractError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function fail(message) {
|
|
77
|
+
throw new AgenticMessagingV1ContractError(message);
|
|
78
|
+
}
|
|
79
|
+
function object(value, label) {
|
|
80
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || nodeTypes.isProxy(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
81
|
+
return fail(`${label} must be a plain object`);
|
|
82
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
83
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
84
|
+
if (typeof key !== "string" || descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor))
|
|
85
|
+
return fail(`${label} must contain only enumerable string data properties`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function exactKeys(value, keys, label) {
|
|
90
|
+
const expected = [...keys].sort();
|
|
91
|
+
const observed = Reflect.ownKeys(value).map(String).sort();
|
|
92
|
+
if (expected.length !== observed.length || observed.some((key, index) => key !== expected[index])) {
|
|
93
|
+
fail(`${label} must contain exactly: ${keys.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function denseArray(value, label, minimum, maximum) {
|
|
97
|
+
if (!Array.isArray(value) || nodeTypes.isProxy(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length < minimum || value.length > maximum)
|
|
98
|
+
return fail(`${label} must contain ${minimum} through ${maximum} items`);
|
|
99
|
+
const keys = Reflect.ownKeys(value);
|
|
100
|
+
const expected = new Set(["length", ...Array.from({ length: value.length }, (_item, index) => String(index))]);
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
103
|
+
if (typeof key !== "string" || !expected.has(key) || descriptor === undefined || !("value" in descriptor) || key !== "length" && descriptor.enumerable !== true)
|
|
104
|
+
return fail(`${label} must be a dense array of data properties`);
|
|
105
|
+
}
|
|
106
|
+
if (keys.length !== expected.size)
|
|
107
|
+
return fail(`${label} must be a dense array of data properties`);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function utf8Bytes(value) {
|
|
111
|
+
return new TextEncoder().encode(value).byteLength;
|
|
112
|
+
}
|
|
113
|
+
function isWellFormedUnicode(value) {
|
|
114
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
115
|
+
const unit = value.charCodeAt(index);
|
|
116
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
117
|
+
const next = value.charCodeAt(index + 1);
|
|
118
|
+
if (!(next >= 56320 && next <= 57343))
|
|
119
|
+
return false;
|
|
120
|
+
index += 1;
|
|
121
|
+
} else if (unit >= 56320 && unit <= 57343) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function boundedText(value, label, maximum) {
|
|
128
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value) || utf8Bytes(value) > maximum || value.includes("\x00"))
|
|
129
|
+
return fail(`${label} must be well-formed NUL-free text within ${maximum} UTF-8 bytes`);
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function identifier(value, label, maximum = AGENTIC_MESSAGING_V1_LIMITS.identifierBytes) {
|
|
133
|
+
const result = boundedText(value, label, maximum);
|
|
134
|
+
if (result.length === 0 || /\p{Cc}|\p{Zl}|\p{Zp}/u.test(result) || result !== result.trim()) {
|
|
135
|
+
return fail(`${label} must be a bounded opaque identifier without controls or surrounding space`);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function digest(value, label) {
|
|
140
|
+
const result = boundedText(value, label, 64);
|
|
141
|
+
if (!/^[a-f0-9]{64}$/u.test(result))
|
|
142
|
+
return fail(`${label} must be lowercase SHA-256`);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function routeCandidateId(value, label) {
|
|
146
|
+
const result = identifier(value, label, 70);
|
|
147
|
+
if (!/^route_[a-f0-9]{64}$/u.test(result)) {
|
|
148
|
+
return fail(`${label} must be a canonical source-conversation route ID`);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
function timestamp(value, label) {
|
|
153
|
+
const result = boundedText(value, label, 64);
|
|
154
|
+
const parsed = new Date(result);
|
|
155
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== result) {
|
|
156
|
+
return fail(`${label} must be a canonical UTC timestamp`);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
function bubble(value, label) {
|
|
161
|
+
const record = object(value, label);
|
|
162
|
+
exactKeys(record, ["id", "text", "replyToRef"], label);
|
|
163
|
+
const text = boundedText(record.text, `${label}.text`, AGENTIC_MESSAGING_V1_LIMITS.bubbleBytes);
|
|
164
|
+
if (text.trim().length === 0 || /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/u.test(text)) {
|
|
165
|
+
return fail(`${label}.text must contain visible text and no unsupported controls`);
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
id: identifier(record.id, `${label}.id`, 128),
|
|
169
|
+
text,
|
|
170
|
+
replyToRef: record.replyToRef === null ? null : identifier(record.replyToRef, `${label}.replyToRef`)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function bubbles(value, label) {
|
|
174
|
+
const result = denseArray(value, label, 1, AGENTIC_MESSAGING_V1_LIMITS.bubbles).map((item, index) => bubble(item, `${label}[${index}]`));
|
|
175
|
+
if (new Set(result.map(({ id }) => id)).size !== result.length)
|
|
176
|
+
fail(`${label} repeats a bubble ID`);
|
|
177
|
+
if (result.reduce((sum, item) => sum + utf8Bytes(item.text), 0) > AGENTIC_MESSAGING_V1_LIMITS.totalBubbleBytes) {
|
|
178
|
+
fail(`${label} exceeds the total text bound`);
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze(result);
|
|
181
|
+
}
|
|
182
|
+
function profileState(value, label) {
|
|
183
|
+
if (value !== "current" && value !== "missing" && value !== "stale") {
|
|
184
|
+
return fail(`${label} must be current, missing, or stale`);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
function handoffCore(value) {
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
function handoffDigest(value) {
|
|
192
|
+
return sha256(canonicalJson(handoffCore(value)));
|
|
193
|
+
}
|
|
194
|
+
function parseWrenchMessagingContextBindingV1(value) {
|
|
195
|
+
const record = object(value, "Wrench messaging context binding");
|
|
196
|
+
exactKeys(record, [
|
|
197
|
+
"schemaVersion",
|
|
198
|
+
"format",
|
|
199
|
+
"contractId",
|
|
200
|
+
"contractHash",
|
|
201
|
+
"routeRef",
|
|
202
|
+
"contextRef",
|
|
203
|
+
"exactDataRevision",
|
|
204
|
+
"latestMessageRevision",
|
|
205
|
+
"validatedAt",
|
|
206
|
+
"expiresAt"
|
|
207
|
+
], "Wrench messaging context binding");
|
|
208
|
+
if (record.schemaVersion !== AGENTIC_MESSAGING_V1_SCHEMA_VERSION || record.format !== WRENCH_MESSAGING_CONTEXT_BINDING_V1_FORMAT)
|
|
209
|
+
return fail("Wrench messaging context binding has the wrong schemaVersion or format");
|
|
210
|
+
if (record.contractId !== WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_ID || record.contractHash !== WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_HASH)
|
|
211
|
+
return fail("Wrench messaging context binding has an unsupported contract identity");
|
|
212
|
+
const validatedAt = timestamp(record.validatedAt, "Wrench messaging context binding.validatedAt");
|
|
213
|
+
const expiresAt = timestamp(record.expiresAt, "Wrench messaging context binding.expiresAt");
|
|
214
|
+
const lifetime = Date.parse(expiresAt) - Date.parse(validatedAt);
|
|
215
|
+
if (lifetime <= 0 || lifetime > AGENTIC_MESSAGING_V1_LIMITS.maximumContextLifetimeMilliseconds) {
|
|
216
|
+
return fail("Wrench messaging context binding has an invalid lifetime");
|
|
217
|
+
}
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
220
|
+
format: WRENCH_MESSAGING_CONTEXT_BINDING_V1_FORMAT,
|
|
221
|
+
contractId: WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_ID,
|
|
222
|
+
contractHash: WRENCH_MESSAGING_CONTEXT_BINDING_V1_CONTRACT_HASH,
|
|
223
|
+
routeRef: identifier(record.routeRef, "Wrench messaging context binding.routeRef"),
|
|
224
|
+
contextRef: identifier(record.contextRef, "Wrench messaging context binding.contextRef"),
|
|
225
|
+
exactDataRevision: digest(record.exactDataRevision, "Wrench messaging context binding.exactDataRevision"),
|
|
226
|
+
latestMessageRevision: digest(record.latestMessageRevision, "Wrench messaging context binding.latestMessageRevision"),
|
|
227
|
+
validatedAt,
|
|
228
|
+
expiresAt
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function parseAgentMessageDraftV1(value) {
|
|
232
|
+
const record = object(value, "Agent message draft");
|
|
233
|
+
exactKeys(record, ["schemaVersion", "format", "bubbles"], "Agent message draft");
|
|
234
|
+
if (record.schemaVersion !== AGENTIC_MESSAGING_V1_SCHEMA_VERSION || record.format !== AGENT_MESSAGE_DRAFT_V1_FORMAT)
|
|
235
|
+
return fail("Agent message draft has the wrong schemaVersion or format");
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
238
|
+
format: AGENT_MESSAGE_DRAFT_V1_FORMAT,
|
|
239
|
+
bubbles: bubbles(record.bubbles, "Agent message draft.bubbles")
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function parseAgentMessageHandoffRequestV1(value) {
|
|
243
|
+
const record = object(value, "Agent message handoff request");
|
|
244
|
+
exactKeys(record, ["schemaVersion", "format", "routeCandidateId"], "Agent message handoff request");
|
|
245
|
+
if (record.schemaVersion !== AGENTIC_MESSAGING_V1_SCHEMA_VERSION || record.format !== AGENT_MESSAGE_HANDOFF_REQUEST_V1_FORMAT)
|
|
246
|
+
return fail("Agent message handoff request has the wrong schemaVersion or format");
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
249
|
+
format: AGENT_MESSAGE_HANDOFF_REQUEST_V1_FORMAT,
|
|
250
|
+
routeCandidateId: routeCandidateId(record.routeCandidateId, "Agent message handoff request.routeCandidateId")
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function createAgentMessageHandoffV1(input) {
|
|
254
|
+
const context = parseWrenchMessagingContextBindingV1(input.wrenchContext);
|
|
255
|
+
const draft = parseAgentMessageDraftV1(input.draft);
|
|
256
|
+
const core = Object.freeze({
|
|
257
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
258
|
+
format: AGENT_MESSAGE_HANDOFF_V1_FORMAT,
|
|
259
|
+
createdAt: timestamp(input.createdAt, "Agent message handoff.createdAt"),
|
|
260
|
+
expiresAt: timestamp(input.expiresAt, "Agent message handoff.expiresAt"),
|
|
261
|
+
contact: Object.freeze({
|
|
262
|
+
contactId: identifier(input.contact.contactId, "Agent message handoff.contact.contactId"),
|
|
263
|
+
routeCandidateId: routeCandidateId(input.contact.routeCandidateId, "Agent message handoff.contact.routeCandidateId"),
|
|
264
|
+
sourceId: identifier(input.contact.sourceId, "Agent message handoff.contact.sourceId"),
|
|
265
|
+
conversationId: identifier(input.contact.conversationId, "Agent message handoff.contact.conversationId")
|
|
266
|
+
}),
|
|
267
|
+
evidence: Object.freeze({
|
|
268
|
+
corpusRevision: digest(input.evidence.corpusRevision, "Agent message handoff.evidence.corpusRevision"),
|
|
269
|
+
sourceRevision: digest(input.evidence.sourceRevision, "Agent message handoff.evidence.sourceRevision"),
|
|
270
|
+
profileState: profileState(input.evidence.profileState, "Agent message handoff.evidence.profileState"),
|
|
271
|
+
profileEvidenceRevision: input.evidence.profileEvidenceRevision === null ? null : digest(input.evidence.profileEvidenceRevision, "Agent message handoff.evidence.profileEvidenceRevision")
|
|
272
|
+
}),
|
|
273
|
+
wrench: Object.freeze({
|
|
274
|
+
contractId: context.contractId,
|
|
275
|
+
contractHash: context.contractHash,
|
|
276
|
+
routeRef: context.routeRef,
|
|
277
|
+
routeRefSha256: sha256(context.routeRef),
|
|
278
|
+
contextRef: context.contextRef,
|
|
279
|
+
contextRefSha256: sha256(context.contextRef),
|
|
280
|
+
exactDataRevision: context.exactDataRevision,
|
|
281
|
+
latestMessageRevision: context.latestMessageRevision,
|
|
282
|
+
validatedAt: context.validatedAt,
|
|
283
|
+
contextExpiresAt: context.expiresAt
|
|
284
|
+
}),
|
|
285
|
+
turn: Object.freeze({ bubbles: draft.bubbles }),
|
|
286
|
+
privacy: Object.freeze({
|
|
287
|
+
classification: "private-local",
|
|
288
|
+
excludedFields: Object.freeze([
|
|
289
|
+
"attachments",
|
|
290
|
+
"credentials",
|
|
291
|
+
"provider-coordinates",
|
|
292
|
+
"provider-payloads"
|
|
293
|
+
])
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
const lifetime = Date.parse(core.expiresAt) - Date.parse(core.createdAt);
|
|
297
|
+
if (lifetime <= 0 || lifetime > AGENTIC_MESSAGING_V1_LIMITS.handoffLifetimeMilliseconds || core.createdAt < core.wrench.validatedAt) {
|
|
298
|
+
return fail("Agent message handoff timestamps are inconsistent");
|
|
299
|
+
}
|
|
300
|
+
if (core.expiresAt > core.wrench.contextExpiresAt) {
|
|
301
|
+
return fail("Agent message handoff outlives its Wrench context binding");
|
|
302
|
+
}
|
|
303
|
+
const canonicalSha256 = handoffDigest(core);
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
...core,
|
|
306
|
+
handoffId: `handoff_${canonicalSha256}`,
|
|
307
|
+
integrity: Object.freeze({ algorithm: "sha256", canonicalSha256 })
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function parseAgentMessageHandoffV1(value) {
|
|
311
|
+
const record = object(value, "Agent message handoff");
|
|
312
|
+
exactKeys(record, [
|
|
313
|
+
"schemaVersion",
|
|
314
|
+
"format",
|
|
315
|
+
"handoffId",
|
|
316
|
+
"createdAt",
|
|
317
|
+
"expiresAt",
|
|
318
|
+
"contact",
|
|
319
|
+
"evidence",
|
|
320
|
+
"wrench",
|
|
321
|
+
"turn",
|
|
322
|
+
"privacy",
|
|
323
|
+
"integrity"
|
|
324
|
+
], "Agent message handoff");
|
|
325
|
+
if (record.schemaVersion !== AGENTIC_MESSAGING_V1_SCHEMA_VERSION || record.format !== AGENT_MESSAGE_HANDOFF_V1_FORMAT)
|
|
326
|
+
return fail("Agent message handoff has the wrong schemaVersion or format");
|
|
327
|
+
const contactRecord = object(record.contact, "Agent message handoff.contact");
|
|
328
|
+
exactKeys(contactRecord, ["contactId", "routeCandidateId", "sourceId", "conversationId"], "Agent message handoff.contact");
|
|
329
|
+
const evidenceRecord = object(record.evidence, "Agent message handoff.evidence");
|
|
330
|
+
exactKeys(evidenceRecord, [
|
|
331
|
+
"corpusRevision",
|
|
332
|
+
"sourceRevision",
|
|
333
|
+
"profileState",
|
|
334
|
+
"profileEvidenceRevision"
|
|
335
|
+
], "Agent message handoff.evidence");
|
|
336
|
+
const wrenchRecord = object(record.wrench, "Agent message handoff.wrench");
|
|
337
|
+
exactKeys(wrenchRecord, [
|
|
338
|
+
"contractId",
|
|
339
|
+
"contractHash",
|
|
340
|
+
"routeRef",
|
|
341
|
+
"routeRefSha256",
|
|
342
|
+
"contextRef",
|
|
343
|
+
"contextRefSha256",
|
|
344
|
+
"exactDataRevision",
|
|
345
|
+
"latestMessageRevision",
|
|
346
|
+
"validatedAt",
|
|
347
|
+
"contextExpiresAt"
|
|
348
|
+
], "Agent message handoff.wrench");
|
|
349
|
+
const turnRecord = object(record.turn, "Agent message handoff.turn");
|
|
350
|
+
exactKeys(turnRecord, ["bubbles"], "Agent message handoff.turn");
|
|
351
|
+
const privacyRecord = object(record.privacy, "Agent message handoff.privacy");
|
|
352
|
+
exactKeys(privacyRecord, ["classification", "excludedFields"], "Agent message handoff.privacy");
|
|
353
|
+
const excluded = denseArray(privacyRecord.excludedFields, "Agent message handoff.privacy.excludedFields", 4, 4);
|
|
354
|
+
const expectedExcluded = ["attachments", "credentials", "provider-coordinates", "provider-payloads"];
|
|
355
|
+
if (excluded.some((item, index) => item !== expectedExcluded[index])) {
|
|
356
|
+
return fail("Agent message handoff.privacy.excludedFields is not canonical");
|
|
357
|
+
}
|
|
358
|
+
if (privacyRecord.classification !== "private-local") {
|
|
359
|
+
return fail("Agent message handoff must be classified private-local");
|
|
360
|
+
}
|
|
361
|
+
const integrityRecord = object(record.integrity, "Agent message handoff.integrity");
|
|
362
|
+
exactKeys(integrityRecord, ["algorithm", "canonicalSha256"], "Agent message handoff.integrity");
|
|
363
|
+
if (integrityRecord.algorithm !== "sha256")
|
|
364
|
+
return fail("Agent message handoff integrity algorithm must be sha256");
|
|
365
|
+
const context = parseWrenchMessagingContextBindingV1({
|
|
366
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
367
|
+
format: WRENCH_MESSAGING_CONTEXT_BINDING_V1_FORMAT,
|
|
368
|
+
contractId: wrenchRecord.contractId,
|
|
369
|
+
contractHash: wrenchRecord.contractHash,
|
|
370
|
+
routeRef: wrenchRecord.routeRef,
|
|
371
|
+
contextRef: wrenchRecord.contextRef,
|
|
372
|
+
exactDataRevision: wrenchRecord.exactDataRevision,
|
|
373
|
+
latestMessageRevision: wrenchRecord.latestMessageRevision,
|
|
374
|
+
validatedAt: wrenchRecord.validatedAt,
|
|
375
|
+
expiresAt: wrenchRecord.contextExpiresAt
|
|
376
|
+
});
|
|
377
|
+
const handoff = createAgentMessageHandoffV1({
|
|
378
|
+
createdAt: timestamp(record.createdAt, "Agent message handoff.createdAt"),
|
|
379
|
+
expiresAt: timestamp(record.expiresAt, "Agent message handoff.expiresAt"),
|
|
380
|
+
contact: {
|
|
381
|
+
contactId: identifier(contactRecord.contactId, "Agent message handoff.contact.contactId"),
|
|
382
|
+
routeCandidateId: routeCandidateId(contactRecord.routeCandidateId, "Agent message handoff.contact.routeCandidateId"),
|
|
383
|
+
sourceId: identifier(contactRecord.sourceId, "Agent message handoff.contact.sourceId"),
|
|
384
|
+
conversationId: identifier(contactRecord.conversationId, "Agent message handoff.contact.conversationId")
|
|
385
|
+
},
|
|
386
|
+
evidence: {
|
|
387
|
+
corpusRevision: digest(evidenceRecord.corpusRevision, "Agent message handoff.evidence.corpusRevision"),
|
|
388
|
+
sourceRevision: digest(evidenceRecord.sourceRevision, "Agent message handoff.evidence.sourceRevision"),
|
|
389
|
+
profileState: profileState(evidenceRecord.profileState, "Agent message handoff.evidence.profileState"),
|
|
390
|
+
profileEvidenceRevision: evidenceRecord.profileEvidenceRevision === null ? null : digest(evidenceRecord.profileEvidenceRevision, "Agent message handoff.evidence.profileEvidenceRevision")
|
|
391
|
+
},
|
|
392
|
+
wrenchContext: context,
|
|
393
|
+
draft: {
|
|
394
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
395
|
+
format: AGENT_MESSAGE_DRAFT_V1_FORMAT,
|
|
396
|
+
bubbles: bubbles(turnRecord.bubbles, "Agent message handoff.turn.bubbles")
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
const suppliedRouteSha = digest(wrenchRecord.routeRefSha256, "Agent message handoff.wrench.routeRefSha256");
|
|
400
|
+
const suppliedContextSha = digest(wrenchRecord.contextRefSha256, "Agent message handoff.wrench.contextRefSha256");
|
|
401
|
+
const suppliedCanonicalSha = digest(integrityRecord.canonicalSha256, "Agent message handoff.integrity.canonicalSha256");
|
|
402
|
+
if (suppliedRouteSha !== handoff.wrench.routeRefSha256 || suppliedContextSha !== handoff.wrench.contextRefSha256 || suppliedCanonicalSha !== handoff.integrity.canonicalSha256 || record.handoffId !== handoff.handoffId)
|
|
403
|
+
return fail("Agent message handoff integrity does not match its canonical content");
|
|
404
|
+
return handoff;
|
|
405
|
+
}
|
|
406
|
+
function wrenchMessagingTurnDigestV1(value) {
|
|
407
|
+
const handoff = parseAgentMessageHandoffV1(value);
|
|
408
|
+
return sha256(canonicalJson({
|
|
409
|
+
schemaVersion: 1,
|
|
410
|
+
format: "wrench.messaging-turn",
|
|
411
|
+
clientIntentSha256: handoff.integrity.canonicalSha256,
|
|
412
|
+
routeRef: handoff.wrench.routeRef,
|
|
413
|
+
contextRef: handoff.wrench.contextRef,
|
|
414
|
+
parts: handoff.turn.bubbles.map((bubble2) => ({
|
|
415
|
+
partId: bubble2.id,
|
|
416
|
+
text: bubble2.text,
|
|
417
|
+
replyRef: bubble2.replyToRef
|
|
418
|
+
}))
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
function parseWrenchMessagingReceiptBindingV1(value) {
|
|
422
|
+
const record = object(value, "Wrench messaging receipt binding");
|
|
423
|
+
exactKeys(record, [
|
|
424
|
+
"schemaVersion",
|
|
425
|
+
"format",
|
|
426
|
+
"contractId",
|
|
427
|
+
"contractHash",
|
|
428
|
+
"clientIntentSha256",
|
|
429
|
+
"routeRefSha256",
|
|
430
|
+
"contextRefSha256",
|
|
431
|
+
"turnDigest",
|
|
432
|
+
"previewDigest",
|
|
433
|
+
"runId",
|
|
434
|
+
"state",
|
|
435
|
+
"partCount",
|
|
436
|
+
"provenPartCount",
|
|
437
|
+
"receiptSha256",
|
|
438
|
+
"recordedAt"
|
|
439
|
+
], "Wrench messaging receipt binding");
|
|
440
|
+
if (record.schemaVersion !== AGENTIC_MESSAGING_V1_SCHEMA_VERSION || record.format !== WRENCH_MESSAGING_RECEIPT_BINDING_V1_FORMAT)
|
|
441
|
+
return fail("Wrench messaging receipt binding has the wrong schemaVersion or format");
|
|
442
|
+
if (record.contractId !== WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_ID || record.contractHash !== WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_HASH)
|
|
443
|
+
return fail("Wrench messaging receipt binding has an unsupported contract identity");
|
|
444
|
+
if (record.state !== "submitted" && record.state !== "failed" && record.state !== "partial" && record.state !== "indeterminate")
|
|
445
|
+
return fail("Wrench messaging receipt binding has an invalid state");
|
|
446
|
+
if (!Number.isSafeInteger(record.partCount) || record.partCount < 1 || record.partCount > 8) {
|
|
447
|
+
return fail("Wrench messaging receipt binding.partCount must be from 1 through 8");
|
|
448
|
+
}
|
|
449
|
+
if (!Number.isSafeInteger(record.provenPartCount) || record.provenPartCount < 0 || record.provenPartCount > record.partCount)
|
|
450
|
+
return fail("Wrench messaging receipt binding.provenPartCount is out of range");
|
|
451
|
+
const partCount = record.partCount;
|
|
452
|
+
const provenPartCount = record.provenPartCount;
|
|
453
|
+
if (record.state === "submitted" && provenPartCount !== partCount || record.state === "failed" && provenPartCount !== 0 || record.state === "partial" && (provenPartCount < 1 || provenPartCount >= partCount) || record.state === "indeterminate" && provenPartCount >= partCount)
|
|
454
|
+
return fail("Wrench messaging receipt binding state does not match its proven prefix");
|
|
455
|
+
const parsed = Object.freeze({
|
|
456
|
+
schemaVersion: AGENTIC_MESSAGING_V1_SCHEMA_VERSION,
|
|
457
|
+
format: WRENCH_MESSAGING_RECEIPT_BINDING_V1_FORMAT,
|
|
458
|
+
contractId: WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_ID,
|
|
459
|
+
contractHash: WRENCH_MESSAGING_RECEIPT_BINDING_V1_CONTRACT_HASH,
|
|
460
|
+
clientIntentSha256: digest(record.clientIntentSha256, "Wrench messaging receipt binding.clientIntentSha256"),
|
|
461
|
+
routeRefSha256: digest(record.routeRefSha256, "Wrench messaging receipt binding.routeRefSha256"),
|
|
462
|
+
contextRefSha256: digest(record.contextRefSha256, "Wrench messaging receipt binding.contextRefSha256"),
|
|
463
|
+
turnDigest: digest(record.turnDigest, "Wrench messaging receipt binding.turnDigest"),
|
|
464
|
+
previewDigest: digest(record.previewDigest, "Wrench messaging receipt binding.previewDigest"),
|
|
465
|
+
runId: identifier(record.runId, "Wrench messaging receipt binding.runId", 256),
|
|
466
|
+
state: record.state,
|
|
467
|
+
partCount,
|
|
468
|
+
provenPartCount,
|
|
469
|
+
receiptSha256: digest(record.receiptSha256, "Wrench messaging receipt binding.receiptSha256"),
|
|
470
|
+
recordedAt: timestamp(record.recordedAt, "Wrench messaging receipt binding.recordedAt")
|
|
471
|
+
});
|
|
472
|
+
const { receiptSha256, ...receiptCore } = parsed;
|
|
473
|
+
if (sha256(canonicalJson(receiptCore)) !== receiptSha256) {
|
|
474
|
+
return fail("Wrench messaging receipt binding receiptSha256 does not match its canonical content");
|
|
475
|
+
}
|
|
476
|
+
return parsed;
|
|
477
|
+
}
|
|
478
|
+
function agentMessageRouteCandidateId(sourceId, conversationId) {
|
|
479
|
+
return `route_${sha256(canonicalJson({
|
|
480
|
+
conversationId: identifier(conversationId, "Source-conversation route.conversationId"),
|
|
481
|
+
sourceId: identifier(sourceId, "Source-conversation route.sourceId")
|
|
482
|
+
}))}`;
|
|
483
|
+
}
|
|
28
484
|
export {
|
|
29
485
|
wrenchMessagingTurnDigestV1,
|
|
30
486
|
parseWrenchMessagingReceiptBindingV1,
|