macp-sdk-typescript 0.10.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 +465 -0
- package/README.md +2 -2
- 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/policy.d.ts +54 -7
- package/dist/policy.js +143 -19
- package/dist/projections/base.d.ts +71 -14
- package/dist/projections/base.js +106 -16
- package/dist/projections/decision.d.ts +5 -49
- package/dist/projections/decision.js +30 -85
- package/dist/projections/handoff.d.ts +5 -48
- package/dist/projections/handoff.js +44 -93
- package/dist/projections/proposal.d.ts +5 -48
- package/dist/projections/proposal.js +21 -79
- package/dist/projections/quorum.d.ts +5 -49
- package/dist/projections/quorum.js +20 -82
- package/dist/projections/task.d.ts +31 -51
- package/dist/projections/task.js +49 -90
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -2
- package/dist/proto-registry.d.ts +85 -5
- package/dist/proto-registry.js +101 -15
- 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 +6 -5
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);
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { PolicyDescriptor } from './types';
|
|
2
2
|
export interface CommitmentRules {
|
|
3
3
|
authority?: 'initiator_only' | 'any_participant' | 'designated_role';
|
|
4
|
+
/**
|
|
5
|
+
* Required non-empty when `authority` is `'designated_role'` — every
|
|
6
|
+
* builder throws `MacpSessionError` otherwise, since an authority rule
|
|
7
|
+
* naming no one is unsatisfiable. Ignored (serialized but inert) under the
|
|
8
|
+
* other two authorities.
|
|
9
|
+
*/
|
|
4
10
|
designatedRoles?: string[];
|
|
5
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Decision-specific: require quorum before commit. Emitted only by
|
|
13
|
+
* `buildDecisionPolicy` (matching `decision-rules.schema.json`'s
|
|
14
|
+
* commitment object, the only one of the five that declares this key) —
|
|
15
|
+
* silently dropped, not merely ignored, when passed to the other four
|
|
16
|
+
* builders, so the still-`additionalProperties: false`
|
|
17
|
+
* quorum/proposal/task/handoff commitment schemas are unaffected (issue
|
|
18
|
+
* #87 item 1).
|
|
19
|
+
*/
|
|
6
20
|
requireVoteQuorum?: boolean;
|
|
7
21
|
/**
|
|
8
22
|
* RFC-MACP-0012 schema_version 2, Decision mode only: when `true`, a
|
|
@@ -49,15 +63,20 @@ export interface EvaluationRules {
|
|
|
49
63
|
requiredBeforeVoting?: boolean;
|
|
50
64
|
}
|
|
51
65
|
export interface QuorumThreshold {
|
|
52
|
-
type: 'n_of_m' | 'percentage'
|
|
66
|
+
type: 'n_of_m' | 'percentage';
|
|
53
67
|
/**
|
|
54
68
|
* The approval bar (RFC-MACP-0012 §4.2) — how many APPROVE commitments the
|
|
55
69
|
* request needs, NOT a participation quorum. Scale depends on `type`:
|
|
56
|
-
* - `n_of_m
|
|
57
|
-
* - `percentage`: an **integer
|
|
70
|
+
* - `n_of_m`: an absolute count.
|
|
71
|
+
* - `percentage`: an **integer 1–100**; the runtime computes the bar as
|
|
58
72
|
* `ceil(value / 100 × participants)`. So `75` means "≥ 75% of participants
|
|
59
|
-
* must approve", NOT `0.75`.
|
|
60
|
-
*
|
|
73
|
+
* must approve", NOT `0.75`.
|
|
74
|
+
*
|
|
75
|
+
* Must be a **positive integer** for every `type` (`exclusiveMinimum: 0` in
|
|
76
|
+
* the canonical `quorum-rules.schema.json`, unconditional — a zero approval
|
|
77
|
+
* bar would be trivially satisfied by any ballot set). `buildQuorumPolicy`
|
|
78
|
+
* validates this and throws on a non-integer, non-positive, or (for
|
|
79
|
+
* `percentage`) out-of-[1,100] value.
|
|
61
80
|
*/
|
|
62
81
|
value: number;
|
|
63
82
|
}
|
|
@@ -109,7 +128,35 @@ export interface HandoffPolicyRulesInput {
|
|
|
109
128
|
acceptance?: HandoffAcceptanceRules;
|
|
110
129
|
commitment?: CommitmentRules;
|
|
111
130
|
}
|
|
112
|
-
export
|
|
131
|
+
export interface DecisionPolicyOptions {
|
|
132
|
+
/**
|
|
133
|
+
* RFC-MACP-0012 rule schema version the runtime evaluates this policy
|
|
134
|
+
* under (validated, never re-validated, at admission -- RFC-MACP-0012 §8
|
|
135
|
+
* item 4):
|
|
136
|
+
* - `1`/`2`: an empty decisive tally is fail-*open* (satisfies the voting
|
|
137
|
+
* requirement).
|
|
138
|
+
* - `3`: an empty decisive tally is fail-*closed* for every algorithm
|
|
139
|
+
* except `'none'` (RFC-MACP-0012 §4.1, adopted in spec PR #99).
|
|
140
|
+
*
|
|
141
|
+
* Defaults to `3` (issue #85, decided jointly with `macp-sdk-python`'s
|
|
142
|
+
* identical `schema_version: int = 3` default -- see that repo's issue
|
|
143
|
+
* #65): RFC-MACP-0012's authoring guidance is that new non-`'none'`
|
|
144
|
+
* policies SHOULD declare `schemaVersion: 3`, and the old default of `2`
|
|
145
|
+
* was fail-*open* on an empty tally for exactly the callers who opted into
|
|
146
|
+
* a binding algorithm (`majority`, `unanimous`, etc.) -- silently
|
|
147
|
+
* satisfying a vote that received zero ballots, which contradicts what
|
|
148
|
+
* asking for a binding algorithm means. RFC-MACP-0012 §8 makes the choice
|
|
149
|
+
* reversible and non-retroactive: a stored policy always evaluates under
|
|
150
|
+
* its own declared version forever, so this default change carries no
|
|
151
|
+
* migration risk for existing registered policies -- it only changes what
|
|
152
|
+
* new callers who don't pass `schemaVersion` explicitly get going forward.
|
|
153
|
+
* Pass `1` or `2` explicitly to keep fail-open empty-tally semantics. This
|
|
154
|
+
* is descriptor metadata, not a rule: it is never part of the serialized
|
|
155
|
+
* `rules` JSON.
|
|
156
|
+
*/
|
|
157
|
+
schemaVersion?: 1 | 2 | 3;
|
|
158
|
+
}
|
|
159
|
+
export declare function buildDecisionPolicy(policyId: string, description: string, rules: DecisionPolicyRulesInput, options?: DecisionPolicyOptions): PolicyDescriptor;
|
|
113
160
|
export declare function buildQuorumPolicy(policyId: string, description: string, rules: QuorumPolicyRulesInput): PolicyDescriptor;
|
|
114
161
|
export declare function buildProposalPolicy(policyId: string, description: string, rules: ProposalPolicyRulesInput): PolicyDescriptor;
|
|
115
162
|
export declare function buildTaskPolicy(policyId: string, description: string, rules: TaskPolicyRulesInput): PolicyDescriptor;
|