macp-sdk-typescript 0.11.0 → 0.12.0
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 +371 -0
- package/README.md +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -1
- package/dist/agent/participant.d.ts +47 -4
- package/dist/agent/participant.js +88 -10
- package/dist/agent/strategies.js +23 -7
- package/dist/agent/transports.d.ts +19 -1
- package/dist/agent/transports.js +108 -66
- package/dist/auth.d.ts +15 -4
- package/dist/auth.js +23 -7
- package/dist/base-session.js +13 -3
- package/dist/client.js +58 -15
- package/dist/commitment-hash.d.ts +33 -3
- package/dist/commitment-hash.js +39 -5
- package/dist/decision.d.ts +1 -0
- package/dist/decision.js +11 -9
- package/dist/envelope.js +9 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.js +19 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/projections/base.d.ts +71 -14
- package/dist/projections/base.js +83 -12
- package/dist/projections/decision.js +23 -3
- package/dist/projections/handoff.js +39 -16
- package/dist/projections/proposal.js +16 -2
- package/dist/projections/quorum.js +13 -0
- package/dist/projections/task.d.ts +27 -4
- package/dist/projections/task.js +44 -13
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -2
- package/dist/proto-registry.d.ts +69 -3
- package/dist/proto-registry.js +83 -5
- package/dist/quorum.d.ts +1 -0
- package/dist/quorum.js +4 -4
- package/dist/task.d.ts +1 -0
- package/dist/task.js +24 -11
- package/dist/types.d.ts +14 -7
- package/dist/validation.d.ts +14 -0
- package/dist/validation.js +39 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/client.js
CHANGED
|
@@ -47,8 +47,10 @@ const path = __importStar(require("node:path"));
|
|
|
47
47
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
48
48
|
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
49
49
|
const auth_1 = require("./auth");
|
|
50
|
+
const constants_1 = require("./constants");
|
|
50
51
|
const envelope_1 = require("./envelope");
|
|
51
52
|
const errors_1 = require("./errors");
|
|
53
|
+
const logging_1 = require("./logging");
|
|
52
54
|
const proto_registry_1 = require("./proto-registry");
|
|
53
55
|
const validation_1 = require("./validation");
|
|
54
56
|
class AsyncQueue {
|
|
@@ -110,6 +112,26 @@ function grpcStatusName(code) {
|
|
|
110
112
|
const name = grpc.status[code];
|
|
111
113
|
return typeof name === 'string' ? name : undefined;
|
|
112
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Shared by the five registry-mutation RPCs (issue #107, Python's
|
|
117
|
+
* `_map_registry_mutation_error`, `client.py:347-362`): a `FAILED_PRECONDITION`
|
|
118
|
+
* against a read-only registry is a rejection of the *call*, not a transport
|
|
119
|
+
* fault — surface it as `MacpAckError`, not `MacpTransportError`.
|
|
120
|
+
*/
|
|
121
|
+
const REGISTRY_MUTATION_ACK_STATUSES = { FAILED_PRECONDITION: 'FAILED_PRECONDITION' };
|
|
122
|
+
/**
|
|
123
|
+
* Convert `grpc.ServiceError.metadata` (a `grpc.Metadata` instance) into the
|
|
124
|
+
* plain `{ key, value }[]` shape {@link MacpAckError}'s constructor expects.
|
|
125
|
+
* `Metadata.getMap()` returns `-bin`-suffixed keys as `Buffer` and all other
|
|
126
|
+
* keys as `string` — exactly the union `MacpAckError.grpcMetadata` declares.
|
|
127
|
+
* Returns `undefined` when the error carries no metadata (e.g. a
|
|
128
|
+
* locally-raised error never sent over the wire).
|
|
129
|
+
*/
|
|
130
|
+
function grpcMetadataEntries(metadata) {
|
|
131
|
+
if (!metadata)
|
|
132
|
+
return undefined;
|
|
133
|
+
return Object.entries(metadata.getMap()).map(([key, value]) => ({ key, value }));
|
|
134
|
+
}
|
|
113
135
|
const TIMEOUT = Symbol('stream-read-timeout');
|
|
114
136
|
const STREAM_END = Symbol('stream-end');
|
|
115
137
|
class MacpStream {
|
|
@@ -120,15 +142,23 @@ class MacpStream {
|
|
|
120
142
|
constructor(call) {
|
|
121
143
|
this.call = call;
|
|
122
144
|
call.on('data', (chunk) => {
|
|
123
|
-
//
|
|
124
|
-
|
|
145
|
+
// `StreamSessionResponse.response` is a proto3 `oneof` (envelope | error).
|
|
146
|
+
// Under this file's own proto-loader options (`oneofs: true`), the
|
|
147
|
+
// decoded object exposes the payload directly as `chunk.envelope` /
|
|
148
|
+
// `chunk.error`, with `chunk.response` set to the oneof's ARM NAME as a
|
|
149
|
+
// *string* (`'envelope'` or `'error'`) — verified against a real
|
|
150
|
+
// StreamSessionResponse encode/decode roundtrip. So `chunk.response.error`
|
|
151
|
+
// indexes a string and is always `undefined`; it never matched a real
|
|
152
|
+
// decoded frame, which is why inline errors were silently dropped.
|
|
153
|
+
const envelope = chunk?.envelope;
|
|
125
154
|
if (envelope) {
|
|
126
155
|
this.queue.push(envelope);
|
|
127
156
|
}
|
|
128
|
-
else if (chunk?.
|
|
129
|
-
// Inline application-level error — stream stays open
|
|
157
|
+
else if (chunk?.error) {
|
|
158
|
+
// Inline application-level error — stream stays open (core.proto).
|
|
159
|
+
logging_1.logger.warn('inline stream error', { code: chunk.error.code, message: chunk.error.message });
|
|
130
160
|
for (const cb of this.inlineErrorCallbacks)
|
|
131
|
-
cb(chunk.
|
|
161
|
+
cb(chunk.error);
|
|
132
162
|
}
|
|
133
163
|
});
|
|
134
164
|
call.on('error', (error) => {
|
|
@@ -288,13 +318,22 @@ class MacpClient {
|
|
|
288
318
|
const resolved = deadlineMs ?? this.defaultDeadlineMs;
|
|
289
319
|
return resolved ? new Date(Date.now() + resolved) : undefined;
|
|
290
320
|
}
|
|
291
|
-
unary(method, request, auth, deadlineMs) {
|
|
321
|
+
unary(method, request, auth, deadlineMs, ackStatuses) {
|
|
292
322
|
return new Promise((resolve, reject) => {
|
|
293
323
|
const callback = (error, response) => {
|
|
294
|
-
if (error)
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
if (error) {
|
|
325
|
+
const statusName = grpcStatusName(error.code);
|
|
326
|
+
const mappedCode = statusName ? ackStatuses?.[statusName] : undefined;
|
|
327
|
+
if (mappedCode) {
|
|
328
|
+
reject(new errors_1.MacpAckError({ ok: false, error: { code: mappedCode, message: error.details || error.message } }, grpcMetadataEntries(error.metadata)));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
reject(new errors_1.MacpTransportError(error.details || error.message, statusName));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
297
335
|
resolve(response);
|
|
336
|
+
}
|
|
298
337
|
};
|
|
299
338
|
const deadline = this.deadline(deadlineMs);
|
|
300
339
|
const metadata = this.metadata(auth);
|
|
@@ -332,7 +371,11 @@ class MacpClient {
|
|
|
332
371
|
}
|
|
333
372
|
async send(envelope, options) {
|
|
334
373
|
const auth = this.requireAuth(options?.auth);
|
|
335
|
-
const response = await this.unary('Send', { envelope }, auth, options?.deadlineMs
|
|
374
|
+
const response = await this.unary('Send', { envelope }, auth, options?.deadlineMs,
|
|
375
|
+
// gRPC ALREADY_EXISTS/FAILED_PRECONDITION on Send are application-level
|
|
376
|
+
// rejections the runtime couldn't express as a normal NACK (issue #107);
|
|
377
|
+
// surface them as MacpAckError, matching macp-sdk-python (client.py:442-452).
|
|
378
|
+
{ ALREADY_EXISTS: constants_1.SESSION_ALREADY_EXISTS, FAILED_PRECONDITION: constants_1.POLICY_DENIED });
|
|
336
379
|
const ack = response.ack;
|
|
337
380
|
// Duplicate acks are success — the message was already accepted
|
|
338
381
|
if (ack?.duplicate)
|
|
@@ -428,23 +471,23 @@ class MacpClient {
|
|
|
428
471
|
'a mode without a terminal Commitment can never resolve and the runtime rejects it.');
|
|
429
472
|
}
|
|
430
473
|
const auth = this.requireAuth(options?.auth);
|
|
431
|
-
return this.unary('RegisterExtMode', { modeDescriptor: descriptor }, auth, options?.deadlineMs);
|
|
474
|
+
return this.unary('RegisterExtMode', { modeDescriptor: descriptor }, auth, options?.deadlineMs, REGISTRY_MUTATION_ACK_STATUSES);
|
|
432
475
|
}
|
|
433
476
|
async unregisterExtMode(mode, options) {
|
|
434
477
|
const auth = this.requireAuth(options?.auth);
|
|
435
|
-
return this.unary('UnregisterExtMode', { mode }, auth, options?.deadlineMs);
|
|
478
|
+
return this.unary('UnregisterExtMode', { mode }, auth, options?.deadlineMs, REGISTRY_MUTATION_ACK_STATUSES);
|
|
436
479
|
}
|
|
437
480
|
async promoteMode(mode, promotedModeName = '', options) {
|
|
438
481
|
const auth = this.requireAuth(options?.auth);
|
|
439
|
-
return this.unary('PromoteMode', { mode, promotedModeName }, auth, options?.deadlineMs);
|
|
482
|
+
return this.unary('PromoteMode', { mode, promotedModeName }, auth, options?.deadlineMs, REGISTRY_MUTATION_ACK_STATUSES);
|
|
440
483
|
}
|
|
441
484
|
async registerPolicy(descriptor, options) {
|
|
442
485
|
const auth = this.requireAuth(options?.auth);
|
|
443
|
-
return this.unary('RegisterPolicy', { policyDescriptor: descriptor }, auth, options?.deadlineMs);
|
|
486
|
+
return this.unary('RegisterPolicy', { policyDescriptor: descriptor }, auth, options?.deadlineMs, REGISTRY_MUTATION_ACK_STATUSES);
|
|
444
487
|
}
|
|
445
488
|
async unregisterPolicy(policyId, options) {
|
|
446
489
|
const auth = this.requireAuth(options?.auth);
|
|
447
|
-
return this.unary('UnregisterPolicy', { policyId }, auth, options?.deadlineMs);
|
|
490
|
+
return this.unary('UnregisterPolicy', { policyId }, auth, options?.deadlineMs, REGISTRY_MUTATION_ACK_STATUSES);
|
|
448
491
|
}
|
|
449
492
|
async getPolicy(policyId, options) {
|
|
450
493
|
const auth = this.requireAuth(options?.auth);
|
|
@@ -14,9 +14,24 @@ import type { CommitmentPayload } from './types';
|
|
|
14
14
|
* that is locale-sensitive and not equivalent to code-unit ordering; it is
|
|
15
15
|
* moot only because the names above are already in the right order.
|
|
16
16
|
*
|
|
17
|
-
* `supersedes` is omitted entirely when `payload.supersedes
|
|
18
|
-
* (D2.3) — including when the payload itself is missing/malformed,
|
|
19
|
-
* ?? {}` never manufactures a `supersedes` key.
|
|
17
|
+
* `supersedes` is omitted entirely when `payload.supersedes` is `undefined`
|
|
18
|
+
* OR `null` (D2.3) — including when the payload itself is missing/malformed,
|
|
19
|
+
* `payload ?? {}` never manufactures a `supersedes` key. `supersedes` is a
|
|
20
|
+
* proto3 optional/message-typed field: its only two wire states are present
|
|
21
|
+
* and absent, and `null` is the idiomatic in-language representation of
|
|
22
|
+
* "absent" for a message-typed field (e.g. under a `defaults: true`
|
|
23
|
+
* protobufjs/proto-loader decode, or a payload round-tripped through JSON).
|
|
24
|
+
* The comparison below is deliberately `!= null` (loose), not `!==
|
|
25
|
+
* undefined` — do not tighten it back; that would resurrect issue #105,
|
|
26
|
+
* where a `null` `supersedes` hashed as present-but-empty instead of absent,
|
|
27
|
+
* diverging from `macp-sdk-python`'s `HasField`-based projection for the
|
|
28
|
+
* same logical commitment. This is deliberately more tolerant than
|
|
29
|
+
* `buildCommitmentPayload` (`envelope.ts`), which still throws on an
|
|
30
|
+
* explicit `supersedes: null` — that function validates a caller's stated
|
|
31
|
+
* intent and should reject a likely mistake; this one (D3) must tolerate
|
|
32
|
+
* whatever a `CommitmentPayload` from any source already contains,
|
|
33
|
+
* including a `null` that is a legitimate "absent" from elsewhere (e.g. a
|
|
34
|
+
* `defaults: true` decode). The two are not meant to converge. An explicit
|
|
20
35
|
* `supersedes: { sessionId: '', commitmentHash: '' }` is NOT collapsed into
|
|
21
36
|
* omission; it is present with two empty-string members, `commitment_hash`
|
|
22
37
|
* before `session_id` (also alphabetical).
|
|
@@ -39,3 +54,18 @@ export declare function canonicalizeCommitmentPayload(payload: CommitmentPayload
|
|
|
39
54
|
* failure rather than being silently swallowed by a catch-all.
|
|
40
55
|
*/
|
|
41
56
|
export declare function commitmentHash(payload: CommitmentPayload): string;
|
|
57
|
+
/**
|
|
58
|
+
* The same shape `commitmentHash` always produces: `sha256:` followed by 64
|
|
59
|
+
* lowercase hex digits. Non-throwing counterpart to `validateCommitmentHash`
|
|
60
|
+
* (`src/validation.ts`, which throws `MacpSessionError` on mismatch) — use
|
|
61
|
+
* this predicate where a boolean fits better than a thrown exception, e.g.
|
|
62
|
+
* filtering a list or a non-fatal check. Both share one regex family
|
|
63
|
+
* (`/^sha256:[0-9a-f]{64}$/`); keep them in sync if either changes.
|
|
64
|
+
*
|
|
65
|
+
* Never throws, for any input. `$` without the `m` flag anchors to the
|
|
66
|
+
* absolute end of the string (not "before a trailing newline", unlike some
|
|
67
|
+
* regex engines) — so a trailing `\n` after an otherwise-valid hash already
|
|
68
|
+
* fails to match with no special-casing needed, matching Python's
|
|
69
|
+
* `fullmatch` semantics for this pattern.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isCanonicalCommitmentHash(value: string): boolean;
|
package/dist/commitment-hash.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.canonicalizeCommitmentPayload = canonicalizeCommitmentPayload;
|
|
4
4
|
exports.commitmentHash = commitmentHash;
|
|
5
|
+
exports.isCanonicalCommitmentHash = isCanonicalCommitmentHash;
|
|
5
6
|
/**
|
|
6
7
|
* RFC-MACP-0013 canonical commitment hashing.
|
|
7
8
|
*
|
|
@@ -141,9 +142,24 @@ function jcsBoolean(value) {
|
|
|
141
142
|
* that is locale-sensitive and not equivalent to code-unit ordering; it is
|
|
142
143
|
* moot only because the names above are already in the right order.
|
|
143
144
|
*
|
|
144
|
-
* `supersedes` is omitted entirely when `payload.supersedes
|
|
145
|
-
* (D2.3) — including when the payload itself is missing/malformed,
|
|
146
|
-
* ?? {}` never manufactures a `supersedes` key.
|
|
145
|
+
* `supersedes` is omitted entirely when `payload.supersedes` is `undefined`
|
|
146
|
+
* OR `null` (D2.3) — including when the payload itself is missing/malformed,
|
|
147
|
+
* `payload ?? {}` never manufactures a `supersedes` key. `supersedes` is a
|
|
148
|
+
* proto3 optional/message-typed field: its only two wire states are present
|
|
149
|
+
* and absent, and `null` is the idiomatic in-language representation of
|
|
150
|
+
* "absent" for a message-typed field (e.g. under a `defaults: true`
|
|
151
|
+
* protobufjs/proto-loader decode, or a payload round-tripped through JSON).
|
|
152
|
+
* The comparison below is deliberately `!= null` (loose), not `!==
|
|
153
|
+
* undefined` — do not tighten it back; that would resurrect issue #105,
|
|
154
|
+
* where a `null` `supersedes` hashed as present-but-empty instead of absent,
|
|
155
|
+
* diverging from `macp-sdk-python`'s `HasField`-based projection for the
|
|
156
|
+
* same logical commitment. This is deliberately more tolerant than
|
|
157
|
+
* `buildCommitmentPayload` (`envelope.ts`), which still throws on an
|
|
158
|
+
* explicit `supersedes: null` — that function validates a caller's stated
|
|
159
|
+
* intent and should reject a likely mistake; this one (D3) must tolerate
|
|
160
|
+
* whatever a `CommitmentPayload` from any source already contains,
|
|
161
|
+
* including a `null` that is a legitimate "absent" from elsewhere (e.g. a
|
|
162
|
+
* `defaults: true` decode). The two are not meant to converge. An explicit
|
|
147
163
|
* `supersedes: { sessionId: '', commitmentHash: '' }` is NOT collapsed into
|
|
148
164
|
* omission; it is present with two empty-string members, `commitment_hash`
|
|
149
165
|
* before `session_id` (also alphabetical).
|
|
@@ -167,9 +183,10 @@ function canonicalizeCommitmentPayload(payload) {
|
|
|
167
183
|
`"policy_version":${jcsString(p.policyVersion ?? '')}`,
|
|
168
184
|
`"reason":${jcsString(p.reason)}`,
|
|
169
185
|
];
|
|
170
|
-
if (p.supersedes
|
|
186
|
+
if (p.supersedes != null) {
|
|
171
187
|
// Optional chaining tolerates `supersedes` being present but not
|
|
172
|
-
// actually an object (
|
|
188
|
+
// actually an object (a primitive: `5`, `'x'`, `true`, ... — `null` is
|
|
189
|
+
// excluded above, it never reaches this branch) on a malformed payload —
|
|
173
190
|
// it never throws, it just yields `undefined` for the sub-fields, which
|
|
174
191
|
// `jcsString` then coerces to `""` the same way it handles any other
|
|
175
192
|
// missing string field.
|
|
@@ -196,3 +213,20 @@ function commitmentHash(payload) {
|
|
|
196
213
|
const digest = (0, node_crypto_1.createHash)('sha256').update(Buffer.from(preimage, 'utf8')).digest('hex');
|
|
197
214
|
return `sha256:${digest}`;
|
|
198
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* The same shape `commitmentHash` always produces: `sha256:` followed by 64
|
|
218
|
+
* lowercase hex digits. Non-throwing counterpart to `validateCommitmentHash`
|
|
219
|
+
* (`src/validation.ts`, which throws `MacpSessionError` on mismatch) — use
|
|
220
|
+
* this predicate where a boolean fits better than a thrown exception, e.g.
|
|
221
|
+
* filtering a list or a non-fatal check. Both share one regex family
|
|
222
|
+
* (`/^sha256:[0-9a-f]{64}$/`); keep them in sync if either changes.
|
|
223
|
+
*
|
|
224
|
+
* Never throws, for any input. `$` without the `m` flag anchors to the
|
|
225
|
+
* absolute end of the string (not "before a trailing newline", unlike some
|
|
226
|
+
* regex engines) — so a trailing `\n` after an otherwise-valid hash already
|
|
227
|
+
* fails to match with no special-casing needed, matching Python's
|
|
228
|
+
* `fullmatch` semantics for this pattern.
|
|
229
|
+
*/
|
|
230
|
+
function isCanonicalCommitmentHash(value) {
|
|
231
|
+
return /^sha256:[0-9a-f]{64}$/.test(value);
|
|
232
|
+
}
|
package/dist/decision.d.ts
CHANGED
package/dist/decision.js
CHANGED
|
@@ -60,10 +60,10 @@ class DecisionSession {
|
|
|
60
60
|
mode: constants_1.MODE_DECISION,
|
|
61
61
|
messageType: 'SessionStart',
|
|
62
62
|
sessionId: this.sessionId,
|
|
63
|
-
sender: this.senderFor(input.sender),
|
|
63
|
+
sender: this.senderFor(input.sender, input.auth),
|
|
64
64
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
|
|
65
65
|
});
|
|
66
|
-
return this.sendAndTrack(envelope, this.auth);
|
|
66
|
+
return this.sendAndTrack(envelope, input.auth ?? this.auth);
|
|
67
67
|
}
|
|
68
68
|
async propose(input) {
|
|
69
69
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
|
@@ -79,39 +79,41 @@ class DecisionSession {
|
|
|
79
79
|
}
|
|
80
80
|
async evaluate(input) {
|
|
81
81
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
|
82
|
-
(
|
|
82
|
+
// Use the validators' returned normalized values (issue #108.3) — they
|
|
83
|
+
// return a normalized string precisely so callers put the wire-canonical
|
|
84
|
+
// form on the envelope, not whatever case the caller happened to pass.
|
|
85
|
+
const recommendation = (0, validation_1.validateRecommendation)(input.recommendation);
|
|
83
86
|
(0, validation_1.validateConfidence)(input.confidence);
|
|
84
87
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
85
88
|
mode: constants_1.MODE_DECISION,
|
|
86
89
|
messageType: 'Evaluation',
|
|
87
90
|
sessionId: this.sessionId,
|
|
88
91
|
sender: this.senderFor(input.sender, input.auth),
|
|
89
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Evaluation', (0, envelope_1.toProtoPayload)(input)),
|
|
92
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Evaluation', (0, envelope_1.toProtoPayload)({ ...input, recommendation })),
|
|
90
93
|
});
|
|
91
94
|
return this.sendAndTrack(envelope, input.auth);
|
|
92
95
|
}
|
|
93
96
|
async raiseObjection(input) {
|
|
94
97
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
|
95
|
-
|
|
96
|
-
(0, validation_1.validateSeverity)(input.severity);
|
|
98
|
+
const severity = input.severity ? (0, validation_1.validateSeverity)(input.severity) : input.severity;
|
|
97
99
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
98
100
|
mode: constants_1.MODE_DECISION,
|
|
99
101
|
messageType: 'Objection',
|
|
100
102
|
sessionId: this.sessionId,
|
|
101
103
|
sender: this.senderFor(input.sender, input.auth),
|
|
102
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Objection', (0, envelope_1.toProtoPayload)(input)),
|
|
104
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Objection', (0, envelope_1.toProtoPayload)({ ...input, severity })),
|
|
103
105
|
});
|
|
104
106
|
return this.sendAndTrack(envelope, input.auth);
|
|
105
107
|
}
|
|
106
108
|
async vote(input) {
|
|
107
109
|
(0, validation_1.validateRequiredField)('proposalId', input.proposalId);
|
|
108
|
-
(0, validation_1.validateVote)(input.vote);
|
|
110
|
+
const vote = (0, validation_1.validateVote)(input.vote);
|
|
109
111
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
110
112
|
mode: constants_1.MODE_DECISION,
|
|
111
113
|
messageType: 'Vote',
|
|
112
114
|
sessionId: this.sessionId,
|
|
113
115
|
sender: this.senderFor(input.sender, input.auth),
|
|
114
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Vote', (0, envelope_1.toProtoPayload)(input)),
|
|
116
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_DECISION, 'Vote', (0, envelope_1.toProtoPayload)({ ...input, vote })),
|
|
115
117
|
});
|
|
116
118
|
return this.sendAndTrack(envelope, input.auth);
|
|
117
119
|
}
|
package/dist/envelope.js
CHANGED
|
@@ -92,7 +92,15 @@ function buildCommitmentPayload(input) {
|
|
|
92
92
|
if (typeof input.supersedes !== 'object' || input.supersedes === null) {
|
|
93
93
|
// `String()`, not `JSON.stringify()`: the latter throws on a bigint,
|
|
94
94
|
// which would replace this deliberate `MacpSessionError` with an
|
|
95
|
-
// unrelated `TypeError` for that one input shape.
|
|
95
|
+
// unrelated `TypeError` for that one input shape. Deliberately
|
|
96
|
+
// stricter here than `commitmentHash`'s D3 "never throw" contract
|
|
97
|
+
// (issue #105): this is a constructor validating a caller's own
|
|
98
|
+
// explicit intent, not a hasher that must tolerate any
|
|
99
|
+
// already-materialized `CommitmentPayload` from any source (including
|
|
100
|
+
// one decoded elsewhere with `defaults: true`, where a `null` is a
|
|
101
|
+
// legitimate "absent" reaching the hasher, not a mistake reaching the
|
|
102
|
+
// builder) — so a `null` here throws instead of being silently
|
|
103
|
+
// treated as absent.
|
|
96
104
|
throw new errors_1.MacpSessionError(`supersedes must be a CommitmentRef object, got: ${String(input.supersedes)}`);
|
|
97
105
|
}
|
|
98
106
|
(0, validation_1.validateCommitmentHash)(input.supersedes.commitmentHash);
|
package/dist/handoff.d.ts
CHANGED
package/dist/handoff.js
CHANGED
|
@@ -60,10 +60,10 @@ class HandoffSession {
|
|
|
60
60
|
mode: constants_1.MODE_HANDOFF,
|
|
61
61
|
messageType: 'SessionStart',
|
|
62
62
|
sessionId: this.sessionId,
|
|
63
|
-
sender: this.senderFor(input.sender),
|
|
63
|
+
sender: this.senderFor(input.sender, input.auth),
|
|
64
64
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
|
|
65
65
|
});
|
|
66
|
-
return this.sendAndTrack(envelope, this.auth);
|
|
66
|
+
return this.sendAndTrack(envelope, input.auth ?? this.auth);
|
|
67
67
|
}
|
|
68
68
|
async offer(input) {
|
|
69
69
|
(0, validation_1.validateRequiredField)('handoffId', input.handoffId);
|
|
@@ -80,40 +80,53 @@ class HandoffSession {
|
|
|
80
80
|
}
|
|
81
81
|
async addContext(input) {
|
|
82
82
|
(0, validation_1.validateRequiredField)('handoffId', input.handoffId);
|
|
83
|
+
// Issue #108.6/4f: default contentType, matching Python (`handoff.py:210`).
|
|
84
|
+
// Deliberately `||`, not the `??` `offer()` above uses for `scope`: an
|
|
85
|
+
// explicit empty string must also fall back to the default here.
|
|
86
|
+
const contextInput = { ...input, contentType: input.contentType || 'application/octet-stream' };
|
|
83
87
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
84
88
|
mode: constants_1.MODE_HANDOFF,
|
|
85
89
|
messageType: 'HandoffContext',
|
|
86
90
|
sessionId: this.sessionId,
|
|
87
91
|
sender: this.senderFor(input.sender, input.auth),
|
|
88
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffContext', (0, envelope_1.toProtoPayload)(
|
|
92
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffContext', (0, envelope_1.toProtoPayload)(contextInput)),
|
|
89
93
|
});
|
|
90
94
|
return this.sendAndTrack(envelope, input.auth);
|
|
91
95
|
}
|
|
92
96
|
async acceptHandoff(input) {
|
|
93
97
|
(0, validation_1.validateRequiredField)('handoffId', input.handoffId);
|
|
98
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
94
99
|
// `implicit` is a runtime-emitted-only flag (RFC-MACP-0010 §5.1); the
|
|
95
100
|
// runtime rejects client-submitted accepts with implicit=true. Strip it so
|
|
96
101
|
// a caller can never produce a rejected envelope, regardless of what they
|
|
97
102
|
// pass — belt and braces with the JSDoc "read-only" contract on the type.
|
|
98
103
|
const rest = { ...input };
|
|
99
104
|
delete rest.implicit;
|
|
105
|
+
// Issue #108.5/4e: default acceptedBy to the resolved sender. Applied to
|
|
106
|
+
// `rest` (the object actually encoded below), NOT to `input` — unlike
|
|
107
|
+
// every other action here, this method doesn't route through
|
|
108
|
+
// `toProtoPayload(input)`, so a fallback written against `input` would be
|
|
109
|
+
// silently dropped.
|
|
110
|
+
rest.acceptedBy = input.acceptedBy || sender;
|
|
100
111
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
101
112
|
mode: constants_1.MODE_HANDOFF,
|
|
102
113
|
messageType: 'HandoffAccept',
|
|
103
114
|
sessionId: this.sessionId,
|
|
104
|
-
sender
|
|
115
|
+
sender,
|
|
105
116
|
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffAccept', rest),
|
|
106
117
|
});
|
|
107
118
|
return this.sendAndTrack(envelope, input.auth);
|
|
108
119
|
}
|
|
109
120
|
async decline(input) {
|
|
110
121
|
(0, validation_1.validateRequiredField)('handoffId', input.handoffId);
|
|
122
|
+
const sender = this.senderFor(input.sender, input.auth);
|
|
123
|
+
const declinedBy = input.declinedBy || sender;
|
|
111
124
|
const envelope = (0, envelope_1.buildEnvelope)({
|
|
112
125
|
mode: constants_1.MODE_HANDOFF,
|
|
113
126
|
messageType: 'HandoffDecline',
|
|
114
127
|
sessionId: this.sessionId,
|
|
115
|
-
sender
|
|
116
|
-
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffDecline', (0, envelope_1.toProtoPayload)(input)),
|
|
128
|
+
sender,
|
|
129
|
+
payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffDecline', (0, envelope_1.toProtoPayload)({ ...input, declinedBy })),
|
|
117
130
|
});
|
|
118
131
|
return this.sendAndTrack(envelope, input.auth);
|
|
119
132
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { Auth, type AuthConfig, type BearerAuthOptions } from './auth';
|
|
2
2
|
export * from './base-session';
|
|
3
3
|
export * from './client';
|
|
4
|
-
export { commitmentHash } from './commitment-hash';
|
|
4
|
+
export { commitmentHash, isCanonicalCommitmentHash } from './commitment-hash';
|
|
5
5
|
export * from './constants';
|
|
6
6
|
export * from './decision';
|
|
7
7
|
export * from './envelope';
|
|
8
8
|
export * from './errors';
|
|
9
9
|
export * from './handoff';
|
|
10
|
-
export
|
|
10
|
+
export { configureLogging, logger, type LogLevel, type LogSink } from './logging';
|
|
11
11
|
export * from './policy';
|
|
12
12
|
export * from './projections';
|
|
13
13
|
export * from './proposal';
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.agent = exports.VERSION = exports.commitmentHash = exports.Auth = void 0;
|
|
39
|
+
exports.agent = exports.VERSION = exports.logger = exports.configureLogging = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
40
40
|
// Auth: export only the public constructors and types. The helpers
|
|
41
41
|
// `validateAuth`, `authSender`, `assertSenderMatchesIdentity`, and
|
|
42
42
|
// `metadataFromAuth` are SDK-internal — importable from the `./auth`
|
|
@@ -46,19 +46,30 @@ var auth_1 = require("./auth");
|
|
|
46
46
|
Object.defineProperty(exports, "Auth", { enumerable: true, get: function () { return auth_1.Auth; } });
|
|
47
47
|
__exportStar(require("./base-session"), exports);
|
|
48
48
|
__exportStar(require("./client"), exports);
|
|
49
|
-
// Commitment hash: export
|
|
50
|
-
// is the projection/JCS half of the
|
|
51
|
-
// `./commitment-hash` submodule (as the
|
|
52
|
-
// the top-level surface to match
|
|
53
|
-
// expose its `canonical_projection`
|
|
49
|
+
// Commitment hash: export `commitmentHash` and `isCanonicalCommitmentHash`.
|
|
50
|
+
// `canonicalizeCommitmentPayload` is the projection/JCS half of the
|
|
51
|
+
// algorithm — importable from the `./commitment-hash` submodule (as the
|
|
52
|
+
// vector-runner tests do) but kept off the top-level surface to match
|
|
53
|
+
// python-sdk's public API, which does not expose its `canonical_projection`
|
|
54
|
+
// equivalent either.
|
|
54
55
|
var commitment_hash_1 = require("./commitment-hash");
|
|
55
56
|
Object.defineProperty(exports, "commitmentHash", { enumerable: true, get: function () { return commitment_hash_1.commitmentHash; } });
|
|
57
|
+
Object.defineProperty(exports, "isCanonicalCommitmentHash", { enumerable: true, get: function () { return commitment_hash_1.isCanonicalCommitmentHash; } });
|
|
56
58
|
__exportStar(require("./constants"), exports);
|
|
57
59
|
__exportStar(require("./decision"), exports);
|
|
58
60
|
__exportStar(require("./envelope"), exports);
|
|
59
61
|
__exportStar(require("./errors"), exports);
|
|
60
62
|
__exportStar(require("./handoff"), exports);
|
|
61
|
-
|
|
63
|
+
// Logging: export the public surface only. `_resetLoggingForTests` is a
|
|
64
|
+
// test-only reset helper — excluded here (a one-hop wildcard, unlike
|
|
65
|
+
// `PROJECTION_ANOMALY_FIELD_ORDER`'s two-hop chain via `./projections`,
|
|
66
|
+
// which cannot be excluded this way). `package.json`'s `exports` map
|
|
67
|
+
// exposes only `.`, so this makes it unreachable from the published
|
|
68
|
+
// package, not merely un-barrelled — accepted, since it is test-only by
|
|
69
|
+
// name and by purpose.
|
|
70
|
+
var logging_1 = require("./logging");
|
|
71
|
+
Object.defineProperty(exports, "configureLogging", { enumerable: true, get: function () { return logging_1.configureLogging; } });
|
|
72
|
+
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logging_1.logger; } });
|
|
62
73
|
__exportStar(require("./policy"), exports);
|
|
63
74
|
__exportStar(require("./projections"), exports);
|
|
64
75
|
__exportStar(require("./proposal"), exports);
|
|
@@ -4,9 +4,11 @@ import type { Envelope } from '../types';
|
|
|
4
4
|
* The kind of cardinality anomaly a projection recorded while replaying an
|
|
5
5
|
* accepted transcript. Cross-SDK frozen contract (`macp-sdk-python` adopted
|
|
6
6
|
* the identical field set, snake_case there) — do not add a kind without
|
|
7
|
-
* cross-SDK agreement.
|
|
7
|
+
* cross-SDK agreement. `duplicate_task_accept`/`settled_handoff` were added
|
|
8
|
+
* under exactly that agreement (issue #126/#128; `macp-sdk-python` landed its
|
|
9
|
+
* half in PR #95, merged `32c6437`).
|
|
8
10
|
*/
|
|
9
|
-
export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot';
|
|
11
|
+
export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot' | 'duplicate_task_accept' | 'settled_handoff';
|
|
10
12
|
/**
|
|
11
13
|
* A recorded observation that a second distinct `Vote` from this sender for
|
|
12
14
|
* this proposal (RFC-MACP-0007 §5.3), or a second distinct ballot across
|
|
@@ -31,7 +33,8 @@ export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot';
|
|
|
31
33
|
* `macp-sdk-python`). Do not add, rename, or remove a field without
|
|
32
34
|
* cross-SDK agreement.
|
|
33
35
|
*
|
|
34
|
-
*
|
|
36
|
+
* `DecisionProjection`, `QuorumProjection`, `TaskProjection`, and
|
|
37
|
+
* `HandoffProjection` populate this today.
|
|
35
38
|
*/
|
|
36
39
|
export interface ProjectionAnomaly {
|
|
37
40
|
kind: ProjectionAnomalyKind;
|
|
@@ -39,7 +42,10 @@ export interface ProjectionAnomaly {
|
|
|
39
42
|
messageType: string;
|
|
40
43
|
messageId: string;
|
|
41
44
|
sender: string;
|
|
42
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* `proposal_id` (Decision), `request_id` (Quorum), `task_id` (Task), or
|
|
47
|
+
* `handoff_id` (Handoff) the anomaly targeted.
|
|
48
|
+
*/
|
|
43
49
|
subjectId: string;
|
|
44
50
|
/**
|
|
45
51
|
* Human-readable detail. For a cross-type Quorum duplicate, `messageType`
|
|
@@ -48,12 +54,54 @@ export interface ProjectionAnomaly {
|
|
|
48
54
|
*/
|
|
49
55
|
detail: string;
|
|
50
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The four `ProjectionAnomalyKind` values as named constants — all four are
|
|
59
|
+
* cross-SDK contract, literally pinned by the spec repo's
|
|
60
|
+
* `schemas/parity/contract.json` (`projection_anomaly.kinds`, `contract_version`
|
|
61
|
+
* 1.2.0 — `ANOMALY_DUPLICATE_TASK_ACCEPT`/`ANOMALY_SETTLED_HANDOFF` landed there
|
|
62
|
+
* under issue #126/#128, after this SDK's PR #134 completed the agreement
|
|
63
|
+
* `macp-sdk-python`'s PR #95 started). `satisfies` links each constant to the
|
|
64
|
+
* union above at compile time, so a drifted value fails `npm run check` before
|
|
65
|
+
* it can ever reach `make verify-parity`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ANOMALY_DUPLICATE_VOTE = "duplicate_vote";
|
|
68
|
+
export declare const ANOMALY_DUPLICATE_BALLOT = "duplicate_ballot";
|
|
69
|
+
export declare const ANOMALY_DUPLICATE_TASK_ACCEPT = "duplicate_task_accept";
|
|
70
|
+
export declare const ANOMALY_SETTLED_HANDOFF = "settled_handoff";
|
|
71
|
+
/**
|
|
72
|
+
* Runtime field order for `ProjectionAnomaly`, with the same frozen-set
|
|
73
|
+
* guarantee as `_ProjectionAnomalyFieldSetIsFrozen` above, expressed as a
|
|
74
|
+
* value rather than only a type. The spec repo's
|
|
75
|
+
* `schemas/parity/contract.json` (`projection_anomaly.fields`) pins this
|
|
76
|
+
* field order as a runtime JSON array, so this SDK needs its own runtime
|
|
77
|
+
* array to assert against it (see `tests/parity/contract.test.ts`).
|
|
78
|
+
*
|
|
79
|
+
* **Public, not hidden — corrected during Phase 2's own verification round.**
|
|
80
|
+
* An earlier revision of this docblock claimed this constant was
|
|
81
|
+
* "deliberately NOT added to the public barrel," on the (wrong) assumption
|
|
82
|
+
* that it would follow `canonicalizeCommitmentPayload`'s precedent in
|
|
83
|
+
* `commitment-hash.ts`. That precedent doesn't transfer: `commitment-hash.ts`
|
|
84
|
+
* is barrel-exported via a *named* line in `src/index.ts`
|
|
85
|
+
* (`export { commitmentHash, isCanonicalCommitmentHash } from
|
|
86
|
+
* './commitment-hash'`), which can omit a name on purpose. This module is
|
|
87
|
+
* barrel-exported via `src/projections.ts`'s `export * from
|
|
88
|
+
* './projections/base'` → `src/index.ts`'s `export * from './projections'`
|
|
89
|
+
* — a wildcard re-export chain has no way to omit one name, so this constant
|
|
90
|
+
* reaches `dist/index.d.ts` and the runtime `require('macp-sdk-typescript')`
|
|
91
|
+
* surface regardless of intent (verified: `node -e "require('./dist/index.js')
|
|
92
|
+
* .PROJECTION_ANOMALY_FIELD_ORDER"` prints the array). This is harmless —
|
|
93
|
+
* a small readonly string tuple is no more a surface-area risk than
|
|
94
|
+
* `STANDARD_MODES` — so the fix is documenting reality, not restructuring
|
|
95
|
+
* the export chain to force exclusion. Phase 3's public-API snapshot
|
|
96
|
+
* correctly captures this symbol as part of today's real surface.
|
|
97
|
+
*/
|
|
98
|
+
export declare const PROJECTION_ANOMALY_FIELD_ORDER: readonly ["kind", "mode", "messageType", "messageId", "sender", "subjectId", "detail"];
|
|
51
99
|
/**
|
|
52
100
|
* Abstract base for in-process mode-state tracking — parity with python-sdk's
|
|
53
101
|
* `macp_sdk.base_projection.BaseProjection`. Maintains a shared transcript,
|
|
54
102
|
* phase string, and commitment payload; subclasses override `applyMode`
|
|
55
|
-
* to handle mode-specific envelopes. `
|
|
56
|
-
*
|
|
103
|
+
* to handle mode-specific envelopes. `Commitment` is handled here so custom
|
|
104
|
+
* modes get the common lifecycle for free.
|
|
57
105
|
*/
|
|
58
106
|
export declare abstract class BaseProjection {
|
|
59
107
|
/**
|
|
@@ -84,19 +132,28 @@ export declare abstract class BaseProjection {
|
|
|
84
132
|
/**
|
|
85
133
|
* Cardinality anomalies recorded while replaying this projection's
|
|
86
134
|
* accepted transcript (e.g. a duplicate vote or ballot from the same
|
|
87
|
-
* sender
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `BaseProjection`
|
|
135
|
+
* sender, a duplicate `TaskAccept` competing for an already-held assignee
|
|
136
|
+
* slot, or a `HandoffAccept`/`HandoffDecline` for an already-settled
|
|
137
|
+
* handoff). See `ProjectionAnomaly`. Empty unless a subclass calls
|
|
138
|
+
* `recordAnomaly`. **Corrected 2026-09-25** (this note previously said
|
|
139
|
+
* nothing calls `recordAnomaly` because `DecisionProjection`/
|
|
140
|
+
* `QuorumProjection` didn't extend `BaseProjection` — that was true before
|
|
141
|
+
* issue #91's refactor, and is stale now): both `DecisionProjection`
|
|
142
|
+
* (`decision.ts`) and `QuorumProjection` (`quorum.ts`) extend
|
|
143
|
+
* `BaseProjection` today and DO call the inherited `recordAnomaly` when
|
|
144
|
+
* they discard a duplicate vote/ballot — they just pass a literal
|
|
145
|
+
* `'duplicate_vote'`/`'duplicate_ballot'` string for `kind` rather than
|
|
146
|
+
* this file's `ANOMALY_DUPLICATE_VOTE`/`ANOMALY_DUPLICATE_BALLOT`
|
|
147
|
+
* constants (referencing those constants instead is optional cleanup, not
|
|
148
|
+
* required by anything in this repo). This field and `recordAnomaly` are
|
|
149
|
+
* no longer ext-mode-only — any subclass, built-in or ext-mode, uses them.
|
|
93
150
|
*/
|
|
94
151
|
readonly anomalies: ProjectionAnomaly[];
|
|
95
152
|
protected abstract readonly mode: string;
|
|
96
153
|
/**
|
|
97
154
|
* True once at least one `ProjectionAnomaly` has been recorded — i.e. this
|
|
98
|
-
* projection observed and discarded a
|
|
99
|
-
*
|
|
155
|
+
* projection observed and discarded a cardinality anomaly (see `anomalies`
|
|
156
|
+
* above for the full set of kinds). This getter is exactly
|
|
100
157
|
* `anomalies.length > 0`; it never asserts "this transcript violates the
|
|
101
158
|
* spec" (see `ProjectionAnomaly`'s docblock, and `applyEnvelope`'s "Input
|
|
102
159
|
* contract: accepted-only" section above, for why a projection cannot make
|