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.
Files changed (50) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/README.md +2 -2
  3. package/dist/agent/index.d.ts +1 -1
  4. package/dist/agent/index.js +2 -1
  5. package/dist/agent/participant.d.ts +47 -4
  6. package/dist/agent/participant.js +88 -10
  7. package/dist/agent/strategies.js +23 -7
  8. package/dist/agent/transports.d.ts +19 -1
  9. package/dist/agent/transports.js +108 -66
  10. package/dist/auth.d.ts +15 -4
  11. package/dist/auth.js +23 -7
  12. package/dist/base-session.js +13 -3
  13. package/dist/client.js +58 -15
  14. package/dist/commitment-hash.d.ts +33 -3
  15. package/dist/commitment-hash.js +39 -5
  16. package/dist/decision.d.ts +1 -0
  17. package/dist/decision.js +11 -9
  18. package/dist/envelope.js +9 -1
  19. package/dist/handoff.d.ts +1 -0
  20. package/dist/handoff.js +19 -6
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +18 -7
  23. package/dist/policy.d.ts +54 -7
  24. package/dist/policy.js +143 -19
  25. package/dist/projections/base.d.ts +71 -14
  26. package/dist/projections/base.js +106 -16
  27. package/dist/projections/decision.d.ts +5 -49
  28. package/dist/projections/decision.js +30 -85
  29. package/dist/projections/handoff.d.ts +5 -48
  30. package/dist/projections/handoff.js +44 -93
  31. package/dist/projections/proposal.d.ts +5 -48
  32. package/dist/projections/proposal.js +21 -79
  33. package/dist/projections/quorum.d.ts +5 -49
  34. package/dist/projections/quorum.js +20 -82
  35. package/dist/projections/task.d.ts +31 -51
  36. package/dist/projections/task.js +49 -90
  37. package/dist/proposal.d.ts +1 -0
  38. package/dist/proposal.js +2 -2
  39. package/dist/proto-registry.d.ts +85 -5
  40. package/dist/proto-registry.js +101 -15
  41. package/dist/quorum.d.ts +1 -0
  42. package/dist/quorum.js +4 -4
  43. package/dist/task.d.ts +1 -0
  44. package/dist/task.js +24 -11
  45. package/dist/types.d.ts +14 -7
  46. package/dist/validation.d.ts +14 -0
  47. package/dist/validation.js +39 -5
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. 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
- // Support both old format (chunk.envelope) and new oneof format (chunk.response.envelope)
124
- const envelope = chunk?.response?.envelope ?? chunk?.envelope;
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?.response?.error) {
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.response.error);
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
- reject(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
296
- else
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 === undefined`
18
- * (D2.3) — including when the payload itself is missing/malformed, `payload
19
- * ?? {}` never manufactures a `supersedes` key. An explicit
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;
@@ -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 === undefined`
145
- * (D2.3) — including when the payload itself is missing/malformed, `payload
146
- * ?? {}` never manufactures a `supersedes` key. An explicit
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 !== undefined) {
186
+ if (p.supersedes != null) {
171
187
  // Optional chaining tolerates `supersedes` being present but not
172
- // actually an object (null, a primitive, ...) on a malformed payload —
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
+ }
@@ -32,6 +32,7 @@ export declare class DecisionSession {
32
32
  }[];
33
33
  maxSuspendMs?: number;
34
34
  sender?: string;
35
+ auth?: AuthConfig;
35
36
  }): Promise<Ack>;
36
37
  propose(input: DecisionProposalPayload & {
37
38
  sender?: string;
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
- (0, validation_1.validateRecommendation)(input.recommendation);
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
- if (input.severity)
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
@@ -32,6 +32,7 @@ export declare class HandoffSession {
32
32
  }[];
33
33
  maxSuspendMs?: number;
34
34
  sender?: string;
35
+ auth?: AuthConfig;
35
36
  }): Promise<Ack>;
36
37
  offer(input: HandoffOfferPayload & {
37
38
  sender?: string;
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)(input)),
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: this.senderFor(input.sender, input.auth),
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: this.senderFor(input.sender, input.auth),
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 * from './logging';
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 only `commitmentHash`. `canonicalizeCommitmentPayload`
50
- // is the projection/JCS half of the algorithm — importable from the
51
- // `./commitment-hash` submodule (as the vector-runner tests do) but kept off
52
- // the top-level surface to match python-sdk's public API, which does not
53
- // expose its `canonical_projection` equivalent either.
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
- __exportStar(require("./logging"), exports);
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
- /** Decision-specific: require quorum before commit. Ignored for other modes. */
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' | 'weighted';
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` / `weighted`: an absolute count (or weight sum).
57
- * - `percentage`: an **integer 0–100**; the runtime computes the bar as
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`. `buildQuorumPolicy` validates this range and
60
- * throws on a non-integer or out-of-[0,100] value.
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 declare function buildDecisionPolicy(policyId: string, description: string, rules: DecisionPolicyRulesInput): PolicyDescriptor;
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;