macp-sdk-typescript 0.14.0 → 0.15.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/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
3
+ exports.KNOWN_DENY_CODES = exports.DENY_UNSPECIFIED = exports.DENY_INVALID_RULES = exports.DENY_UNSUPPORTED_SCHEMA_VERSION = exports.DENY_REASSIGNMENT_NOT_ALLOWED = exports.DENY_OUTPUT_REQUIRED = exports.DENY_CRITERION_NOT_MET = exports.DENY_DECLINE_NOT_BACKED = exports.DENY_DECLINE_OVER_APPROVAL = exports.DENY_NO_VOTES = exports.DENY_EVALUATION_REQUIRED = exports.DENY_CONFIDENCE_BELOW_MINIMUM = exports.DENY_THRESHOLD_NOT_MET = exports.DENY_QUORUM_NOT_MET = exports.DENY_VETO_HOLD = exports.DENY_VETO = exports.AGENT_NOT_FOUND = exports.STREAM_LAGGED = exports.INVALID_REQUEST = exports.REGISTRY_READ_ONLY = exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
4
4
  exports.MACP_VERSION = '1.0';
5
5
  exports.DEFAULT_MODE_VERSION = '1.0.0';
6
6
  exports.DEFAULT_CONFIGURATION_VERSION = 'config.default';
@@ -34,3 +34,52 @@ exports.POLICY_DENIED = 'POLICY_DENIED';
34
34
  exports.INVALID_SESSION_ID = 'INVALID_SESSION_ID';
35
35
  exports.UNKNOWN_POLICY_VERSION = 'UNKNOWN_POLICY_VERSION';
36
36
  exports.INVALID_POLICY_DEFINITION = 'INVALID_POLICY_DEFINITION';
37
+ // Control-plane codes added in runtime 0.9.0 (carried in `macp-error-code`
38
+ // metadata; see `MacpTransportError.macpErrorCode` / `MacpAckError.macpErrorCode`).
39
+ /** Register/UnregisterPolicy against a file-loaded (read-only) policy registry — FAILED_PRECONDITION. */
40
+ exports.REGISTRY_READ_ONLY = 'REGISTRY_READ_ONLY';
41
+ /** Malformed control-plane request (bad page_size/page_token, missing descriptor, …) — INVALID_ARGUMENT. */
42
+ exports.INVALID_REQUEST = 'INVALID_REQUEST';
43
+ /** Stream/watch subscriber fell behind — RESOURCE_EXHAUSTED; resubscribe with `afterSequence`. */
44
+ exports.STREAM_LAGGED = 'STREAM_LAGGED';
45
+ /** GetManifest for an agent this runtime does not host — NOT_FOUND. */
46
+ exports.AGENT_NOT_FOUND = 'AGENT_NOT_FOUND';
47
+ // Structured POLICY_DENIED denial codes (`PolicyDenial.code`, runtime ≥ 0.9.0;
48
+ // RFC-MACP-0012 §6.2). Open set — names match python-sdk's exports.
49
+ exports.DENY_VETO = 'veto';
50
+ exports.DENY_VETO_HOLD = 'veto_hold';
51
+ exports.DENY_QUORUM_NOT_MET = 'quorum_not_met';
52
+ exports.DENY_THRESHOLD_NOT_MET = 'threshold_not_met';
53
+ exports.DENY_CONFIDENCE_BELOW_MINIMUM = 'confidence_below_minimum';
54
+ exports.DENY_EVALUATION_REQUIRED = 'evaluation_required';
55
+ exports.DENY_NO_VOTES = 'no_votes';
56
+ exports.DENY_DECLINE_OVER_APPROVAL = 'decline_over_approval';
57
+ exports.DENY_DECLINE_NOT_BACKED = 'decline_not_backed';
58
+ exports.DENY_CRITERION_NOT_MET = 'criterion_not_met';
59
+ exports.DENY_OUTPUT_REQUIRED = 'output_required';
60
+ exports.DENY_REASSIGNMENT_NOT_ALLOWED = 'reassignment_not_allowed';
61
+ exports.DENY_UNSUPPORTED_SCHEMA_VERSION = 'unsupported_schema_version';
62
+ exports.DENY_INVALID_RULES = 'invalid_rules';
63
+ exports.DENY_UNSPECIFIED = 'unspecified';
64
+ /**
65
+ * The denial codes the reference evaluator emits. A custom evaluator may send
66
+ * others — `PolicyDenial.code` passes them through unchanged, so treat a code
67
+ * not in this set as {@link DENY_UNSPECIFIED}.
68
+ */
69
+ exports.KNOWN_DENY_CODES = new Set([
70
+ exports.DENY_VETO,
71
+ exports.DENY_VETO_HOLD,
72
+ exports.DENY_QUORUM_NOT_MET,
73
+ exports.DENY_THRESHOLD_NOT_MET,
74
+ exports.DENY_CONFIDENCE_BELOW_MINIMUM,
75
+ exports.DENY_EVALUATION_REQUIRED,
76
+ exports.DENY_NO_VOTES,
77
+ exports.DENY_DECLINE_OVER_APPROVAL,
78
+ exports.DENY_DECLINE_NOT_BACKED,
79
+ exports.DENY_CRITERION_NOT_MET,
80
+ exports.DENY_OUTPUT_REQUIRED,
81
+ exports.DENY_REASSIGNMENT_NOT_ALLOWED,
82
+ exports.DENY_UNSUPPORTED_SCHEMA_VERSION,
83
+ exports.DENY_INVALID_RULES,
84
+ exports.DENY_UNSPECIFIED,
85
+ ]);
package/dist/decision.js CHANGED
@@ -95,7 +95,10 @@ class DecisionSession {
95
95
  }
96
96
  async raiseObjection(input) {
97
97
  (0, validation_1.validateRequiredField)('proposalId', input.proposalId);
98
- const severity = input.severity ? (0, validation_1.validateSeverity)(input.severity) : input.severity;
98
+ // Always send an explicit lowercase severity: runtime ≥ 0.9.0 sessions
99
+ // (semantics_rev 4) reject an empty one instead of defaulting to
100
+ // "medium", so the SDK applies that default itself (python-sdk parity).
101
+ const severity = (0, validation_1.validateSeverity)(input.severity ?? 'medium');
99
102
  const envelope = (0, envelope_1.buildEnvelope)({
100
103
  mode: constants_1.MODE_DECISION,
101
104
  messageType: 'Objection',
package/dist/errors.d.ts CHANGED
@@ -8,12 +8,62 @@ export declare class MacpTransportError extends MacpSdkError {
8
8
  * `UNAUTHENTICATED`) when the underlying transport failure carried one.
9
9
  * Lets consumers distinguish, for example, watch-stream consumer lag
10
10
  * (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
11
- * → do not reconnect), or a passive-subscribe resume below a compacted base
12
- * (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
11
+ * → do not reconnect). Absent for locally-raised transport errors. For a
12
+ * runtime-raised MACP error prefer {@link macpErrorCode}.
13
13
  */
14
14
  readonly code?: string;
15
- constructor(message: string, code?: string);
15
+ /**
16
+ * MACP error-codes registry code (e.g. `SESSION_NOT_FOUND`, `POLICY_DENIED`)
17
+ * from the status's `macp-error-code` trailing metadata (runtime ≥ 0.9.0).
18
+ * Branch on this rather than {@link code}: since 0.9.0 the gRPC status
19
+ * follows the registry's gRPC column, and several registry codes share one
20
+ * status (e.g. `INVALID_ENVELOPE` and `INVALID_SESSION_ID` are both
21
+ * `INVALID_ARGUMENT`). Absent for older runtimes, for statuses the runtime
22
+ * raised without a registry code, and for locally-raised errors.
23
+ */
24
+ readonly macpErrorCode?: string;
25
+ constructor(message: string, code?: string, macpErrorCode?: string);
26
+ }
27
+ /**
28
+ * One structured `POLICY_DENIED` reason (RFC-MACP-0012 §6.2, runtime ≥ 0.9.0).
29
+ *
30
+ * `code` is an open set: the reference evaluator emits `veto`, `veto_hold`,
31
+ * `quorum_not_met`, `threshold_not_met`, `confidence_below_minimum`,
32
+ * `evaluation_required`, `no_votes`, `decline_over_approval`,
33
+ * `decline_not_backed`, `criterion_not_met`, `output_required`,
34
+ * `reassignment_not_allowed`, `unsupported_schema_version`, `invalid_rules`
35
+ * and `unspecified` (exported as the `DENY_*` constants and
36
+ * {@link KNOWN_DENY_CODES}), but a custom evaluator may mint its own. `code`
37
+ * is the wire value passed through unchanged — treat one not in
38
+ * `KNOWN_DENY_CODES` as `unspecified`. `participants` lists the participant
39
+ * ids the reason concerns (e.g. a veto's objectors); since 0.9.0 they are no
40
+ * longer embedded in `message`.
41
+ */
42
+ export interface PolicyDenial {
43
+ code: string;
44
+ message: string;
45
+ participants: string[];
16
46
  }
47
+ /** Parsed MACP error `details` JSON: legacy `reasons` strings plus structured `denials`. */
48
+ export interface MacpErrorDetails {
49
+ reasons: string[];
50
+ denials: PolicyDenial[];
51
+ }
52
+ /**
53
+ * Parse the JSON `details` the runtime attaches to a `POLICY_DENIED` error —
54
+ * found on `Ack.error.details`, an inline stream `MACPError.details`, or the
55
+ * `macp-error-details-bin` trailing metadata. Never throws: missing,
56
+ * non-JSON, or malformed input yields empty arrays; a `denials` entry that is
57
+ * not an object is dropped, and one without a string `message` is kept with
58
+ * `message: ''`. Pre-0.9.0 runtimes send `reasons` only,
59
+ * so `denials` is `[]` for them.
60
+ */
61
+ export declare function parseErrorDetails(details: Buffer | Uint8Array | string | null | undefined): MacpErrorDetails;
62
+ /** Value of the `macp-error-code` trailing-metadata key, if present (runtime ≥ 0.9.0). */
63
+ export declare function macpErrorCodeFromMetadata(grpcMetadata?: Array<{
64
+ key: string;
65
+ value: string | Buffer;
66
+ }>): string | undefined;
17
67
  /**
18
68
  * Structured NACK record. Mirrors python-sdk's `AckFailure` dataclass so
19
69
  * cross-SDK code can share the same shape when logging or persisting a
@@ -25,6 +75,11 @@ export interface AckFailure {
25
75
  sessionId: string;
26
76
  messageId: string;
27
77
  reasons: string[];
78
+ /**
79
+ * Structured policy-denial reasons (runtime ≥ 0.9.0). Empty for non-policy
80
+ * NACKs and for older runtimes, which send `reasons` strings only.
81
+ */
82
+ denials: PolicyDenial[];
28
83
  }
29
84
  export declare class MacpAckError extends MacpSdkError {
30
85
  readonly ack: Ack;
@@ -35,13 +90,19 @@ export declare class MacpAckError extends MacpSdkError {
35
90
  }>;
36
91
  /** Structured NACK record — parity with python-sdk `MacpAckError.failure`. */
37
92
  readonly failure: AckFailure;
93
+ /**
94
+ * The registry code from the `macp-error-code` metadata (runtime ≥ 0.9.0)
95
+ * when this NACK was synthesized from a gRPC status. Usually equal to
96
+ * `failure.code`; differs on the registry-mutation RPCs, which keep
97
+ * `failure.code: 'FAILED_PRECONDITION'` for compatibility while this names
98
+ * the precise cause (e.g. `REGISTRY_READ_ONLY`).
99
+ */
100
+ readonly macpErrorCode?: string;
38
101
  constructor(ack: Ack, grpcMetadata?: Array<{
39
102
  key: string;
40
103
  value: string | Buffer;
41
104
  }>);
42
- private static _extractReasons;
43
- private static _parseAckReasons;
44
- private static _parseGrpcMetadataReasons;
105
+ private static _extractDetails;
45
106
  }
46
107
  export declare class MacpSessionError extends MacpSdkError {
47
108
  constructor(message: string);
package/dist/errors.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MacpIdentityMismatchError = exports.MacpRetryError = exports.MacpTimeoutError = exports.MacpSessionError = exports.MacpAckError = exports.MacpTransportError = exports.MacpSdkError = void 0;
4
+ exports.parseErrorDetails = parseErrorDetails;
5
+ exports.macpErrorCodeFromMetadata = macpErrorCodeFromMetadata;
4
6
  class MacpSdkError extends Error {
5
7
  constructor(message) {
6
8
  super(message);
@@ -14,80 +16,121 @@ class MacpTransportError extends MacpSdkError {
14
16
  * `UNAUTHENTICATED`) when the underlying transport failure carried one.
15
17
  * Lets consumers distinguish, for example, watch-stream consumer lag
16
18
  * (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
17
- * → do not reconnect), or a passive-subscribe resume below a compacted base
18
- * (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
19
+ * → do not reconnect). Absent for locally-raised transport errors. For a
20
+ * runtime-raised MACP error prefer {@link macpErrorCode}.
19
21
  */
20
22
  code;
21
- constructor(message, code) {
23
+ /**
24
+ * MACP error-codes registry code (e.g. `SESSION_NOT_FOUND`, `POLICY_DENIED`)
25
+ * from the status's `macp-error-code` trailing metadata (runtime ≥ 0.9.0).
26
+ * Branch on this rather than {@link code}: since 0.9.0 the gRPC status
27
+ * follows the registry's gRPC column, and several registry codes share one
28
+ * status (e.g. `INVALID_ENVELOPE` and `INVALID_SESSION_ID` are both
29
+ * `INVALID_ARGUMENT`). Absent for older runtimes, for statuses the runtime
30
+ * raised without a registry code, and for locally-raised errors.
31
+ */
32
+ macpErrorCode;
33
+ constructor(message, code, macpErrorCode) {
22
34
  super(message);
23
35
  this.name = 'MacpTransportError';
24
36
  this.code = code;
37
+ if (macpErrorCode !== undefined)
38
+ this.macpErrorCode = macpErrorCode;
25
39
  }
26
40
  }
27
41
  exports.MacpTransportError = MacpTransportError;
42
+ /**
43
+ * Parse the JSON `details` the runtime attaches to a `POLICY_DENIED` error —
44
+ * found on `Ack.error.details`, an inline stream `MACPError.details`, or the
45
+ * `macp-error-details-bin` trailing metadata. Never throws: missing,
46
+ * non-JSON, or malformed input yields empty arrays; a `denials` entry that is
47
+ * not an object is dropped, and one without a string `message` is kept with
48
+ * `message: ''`. Pre-0.9.0 runtimes send `reasons` only,
49
+ * so `denials` is `[]` for them.
50
+ */
51
+ function parseErrorDetails(details) {
52
+ const empty = { reasons: [], denials: [] };
53
+ if (details === null || details === undefined)
54
+ return empty;
55
+ let parsed;
56
+ try {
57
+ const raw = typeof details === 'string' ? details : Buffer.from(details).toString('utf-8');
58
+ if (raw.length === 0)
59
+ return empty;
60
+ parsed = JSON.parse(raw);
61
+ }
62
+ catch {
63
+ return empty;
64
+ }
65
+ if (typeof parsed !== 'object' || parsed === null)
66
+ return empty;
67
+ const obj = parsed;
68
+ const reasons = Array.isArray(obj.reasons) ? obj.reasons.filter((r) => typeof r === 'string') : [];
69
+ const denials = [];
70
+ if (Array.isArray(obj.denials)) {
71
+ for (const d of obj.denials) {
72
+ if (typeof d !== 'object' || d === null)
73
+ continue;
74
+ const { code, message, participants } = d;
75
+ denials.push({
76
+ code: typeof code === 'string' && code.length > 0 ? code : 'unspecified',
77
+ // A denial without a string message is kept with '' (python-sdk
78
+ // parity, issue #168) — its code and participants still matter.
79
+ message: typeof message === 'string' ? message : '',
80
+ participants: Array.isArray(participants) ? participants.filter((p) => typeof p === 'string') : [],
81
+ });
82
+ }
83
+ }
84
+ return { reasons, denials };
85
+ }
86
+ /** Value of the `macp-error-code` trailing-metadata key, if present (runtime ≥ 0.9.0). */
87
+ function macpErrorCodeFromMetadata(grpcMetadata) {
88
+ const entry = grpcMetadata?.find((item) => item.key === 'macp-error-code');
89
+ if (!entry)
90
+ return undefined;
91
+ const value = typeof entry.value === 'string' ? entry.value : entry.value.toString('utf-8');
92
+ return value.length > 0 ? value : undefined;
93
+ }
28
94
  class MacpAckError extends MacpSdkError {
29
95
  ack;
30
96
  /** Optional gRPC trailing metadata for extracting additional error details. */
31
97
  grpcMetadata;
32
98
  /** Structured NACK record — parity with python-sdk `MacpAckError.failure`. */
33
99
  failure;
100
+ /**
101
+ * The registry code from the `macp-error-code` metadata (runtime ≥ 0.9.0)
102
+ * when this NACK was synthesized from a gRPC status. Usually equal to
103
+ * `failure.code`; differs on the registry-mutation RPCs, which keep
104
+ * `failure.code: 'FAILED_PRECONDITION'` for compatibility while this names
105
+ * the precise cause (e.g. `REGISTRY_READ_ONLY`).
106
+ */
107
+ macpErrorCode;
34
108
  constructor(ack, grpcMetadata) {
35
109
  super(`${ack.error?.code ?? 'UNKNOWN'}: ${ack.error?.message ?? 'runtime returned nack'}`);
36
110
  this.name = 'MacpAckError';
37
111
  this.ack = ack;
38
112
  this.grpcMetadata = grpcMetadata;
39
- const reasons = MacpAckError._extractReasons(ack, grpcMetadata);
113
+ const { reasons, denials } = MacpAckError._extractDetails(ack, grpcMetadata);
40
114
  this.failure = {
41
115
  code: ack.error?.code ?? 'UNKNOWN',
42
116
  message: ack.error?.message ?? 'runtime returned nack',
43
117
  sessionId: ack.sessionId ?? ack.error?.sessionId ?? '',
44
118
  messageId: ack.messageId ?? ack.error?.messageId ?? '',
45
119
  reasons,
120
+ denials,
46
121
  };
122
+ const macpErrorCode = macpErrorCodeFromMetadata(grpcMetadata);
123
+ if (macpErrorCode !== undefined)
124
+ this.macpErrorCode = macpErrorCode;
47
125
  }
48
- static _extractReasons(ack, grpcMetadata) {
49
- // First try: parse from ack.error.details (matches Python _parse_ack_reasons)
50
- const fromDetails = MacpAckError._parseAckReasons(ack);
51
- if (fromDetails.length > 0)
52
- return fromDetails;
53
- // Second try: parse from gRPC trailing metadata (matches Python _parse_grpc_metadata_reasons)
54
- return MacpAckError._parseGrpcMetadataReasons(grpcMetadata);
55
- }
56
- static _parseAckReasons(ack) {
57
- if (!ack.error?.details)
58
- return [];
59
- try {
60
- const details = ack.error.details;
61
- const raw = Buffer.isBuffer(details) ? details.toString('utf-8') : String(details);
62
- const parsed = JSON.parse(raw);
63
- const reasons = parsed.reasons;
64
- return Array.isArray(reasons) ? reasons : [];
65
- }
66
- catch {
67
- return [];
68
- }
69
- }
70
- static _parseGrpcMetadataReasons(grpcMetadata) {
71
- if (!grpcMetadata)
72
- return [];
73
- try {
74
- for (const item of grpcMetadata) {
75
- if (item.key === 'macp-error-details-bin') {
76
- const data = Buffer.isBuffer(item.value)
77
- ? item.value.toString('utf-8')
78
- : typeof item.value === 'string'
79
- ? item.value
80
- : Buffer.from(item.value).toString('utf-8');
81
- const parsed = JSON.parse(data);
82
- const reasons = parsed.reasons;
83
- return Array.isArray(reasons) ? reasons : [];
84
- }
85
- }
86
- }
87
- catch {
88
- // Intentionally swallowed — matches Python's bare except
89
- }
90
- return [];
126
+ static _extractDetails(ack, grpcMetadata) {
127
+ // First try: ack.error.details (matches Python _parse_ack_reasons)
128
+ const fromAck = parseErrorDetails(ack.error?.details);
129
+ if (fromAck.reasons.length > 0 || fromAck.denials.length > 0)
130
+ return fromAck;
131
+ // Second try: gRPC trailing metadata (matches Python _parse_grpc_metadata_reasons)
132
+ const entry = grpcMetadata?.find((item) => item.key === 'macp-error-details-bin');
133
+ return parseErrorDetails(entry?.value);
91
134
  }
92
135
  }
93
136
  exports.MacpAckError = MacpAckError;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { Auth, type AuthConfig, type BearerAuthOptions } from './auth';
2
2
  export * from './base-session';
3
3
  export * from './client';
4
- export { commitmentHash, isCanonicalCommitmentHash } from './commitment-hash';
4
+ export { commitmentHash, isCanonicalCommitmentHash, verifyCommitmentHash, type VerifyOutcome } from './commitment-hash';
5
5
  export * from './constants';
6
6
  export * from './decision';
7
7
  export * from './envelope';
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.logger = exports.configureLogging = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
39
+ exports.agent = exports.VERSION = exports.logger = exports.configureLogging = exports.verifyCommitmentHash = 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,7 +46,8 @@ 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 `commitmentHash` and `isCanonicalCommitmentHash`.
49
+ // Commitment hash: export `commitmentHash`, `isCanonicalCommitmentHash`, and
50
+ // the RFC-MACP-0013 §6.1 verifier `verifyCommitmentHash`.
50
51
  // `canonicalizeCommitmentPayload` is the projection/JCS half of the
51
52
  // algorithm — importable from the `./commitment-hash` submodule (as the
52
53
  // vector-runner tests do) but kept off the top-level surface to match
@@ -55,6 +56,7 @@ __exportStar(require("./client"), exports);
55
56
  var commitment_hash_1 = require("./commitment-hash");
56
57
  Object.defineProperty(exports, "commitmentHash", { enumerable: true, get: function () { return commitment_hash_1.commitmentHash; } });
57
58
  Object.defineProperty(exports, "isCanonicalCommitmentHash", { enumerable: true, get: function () { return commitment_hash_1.isCanonicalCommitmentHash; } });
59
+ Object.defineProperty(exports, "verifyCommitmentHash", { enumerable: true, get: function () { return commitment_hash_1.verifyCommitmentHash; } });
58
60
  __exportStar(require("./constants"), exports);
59
61
  __exportStar(require("./decision"), exports);
60
62
  __exportStar(require("./envelope"), exports);
package/dist/policy.js CHANGED
@@ -105,9 +105,13 @@ function buildDecisionPolicy(policyId, description, rules, options) {
105
105
  }
106
106
  for (const [participant, weight] of Object.entries(weights)) {
107
107
  // weight <= 0 alone would let a NaN weight slip through (NaN <= 0 is
108
- // false); the runtime guards this explicitly (registry.rs:596), mirrored
109
- // here even though the Python reference does not check it separately.
110
- if (weight <= 0 || Number.isNaN(weight)) {
108
+ // false); the runtime guards this explicitly (registry.rs:596).
109
+ // !Number.isFinite(weight) is true for NaN, +Infinity, and -Infinity
110
+ // alike -- the exact mirror of Python's own guard, policy.py:186:
111
+ // `if not math.isfinite(weight) or weight <= 0:` (issue #152; a
112
+ // +Infinity weight previously slipped past the old Number.isNaN-only
113
+ // check and serialized to JSON `null`, which is schema-invalid).
114
+ if (weight <= 0 || !Number.isFinite(weight)) {
111
115
  throw new errors_1.MacpSessionError(`weights['${participant}'] must be > 0, got ${weight} -- a weight-0 observer is expressed by ` +
112
116
  'omission from the map, not by an explicit 0');
113
117
  }
@@ -226,4 +226,18 @@ export declare abstract class BaseProjection {
226
226
  * `MACP_LOG_LEVEL` environment variable.
227
227
  */
228
228
  protected recordAnomaly(anomaly: ProjectionAnomaly): void;
229
+ /**
230
+ * Assign `this.phase`, guarding against regression out of the terminal
231
+ * `'Committed'` state — parity with macp-sdk-python's `_set_phase`
232
+ * (`base_projection.py:289-313`). `applyEnvelope` sets `this.phase =
233
+ * 'Committed'` directly on a `Commitment` envelope and returns before
234
+ * `applyMode` is ever called (see `applyEnvelope` above), so this method is
235
+ * never asked to move *into* `'Committed'` — only ever *out of* it, which is
236
+ * exactly the case it exists to reject. Every subclass `applyMode`
237
+ * implementation must call this instead of assigning `this.phase` directly;
238
+ * the one exception is each subclass's own field initializer, which sets
239
+ * the starting phase before any message has been applied and can never
240
+ * observe `'Committed'`.
241
+ */
242
+ protected setPhase(phase: string): void;
229
243
  }
@@ -249,5 +249,23 @@ class BaseProjection {
249
249
  this.anomalies.push(anomaly);
250
250
  logging_1.logger.warn('projection anomaly', anomaly);
251
251
  }
252
+ /**
253
+ * Assign `this.phase`, guarding against regression out of the terminal
254
+ * `'Committed'` state — parity with macp-sdk-python's `_set_phase`
255
+ * (`base_projection.py:289-313`). `applyEnvelope` sets `this.phase =
256
+ * 'Committed'` directly on a `Commitment` envelope and returns before
257
+ * `applyMode` is ever called (see `applyEnvelope` above), so this method is
258
+ * never asked to move *into* `'Committed'` — only ever *out of* it, which is
259
+ * exactly the case it exists to reject. Every subclass `applyMode`
260
+ * implementation must call this instead of assigning `this.phase` directly;
261
+ * the one exception is each subclass's own field initializer, which sets
262
+ * the starting phase before any message has been applied and can never
263
+ * observe `'Committed'`.
264
+ */
265
+ setPhase(phase) {
266
+ if (this.phase === 'Committed')
267
+ return;
268
+ this.phase = phase;
269
+ }
252
270
  }
253
271
  exports.BaseProjection = BaseProjection;
@@ -22,7 +22,15 @@ class DecisionProjection extends base_1.BaseProjection {
22
22
  rationale: record.rationale,
23
23
  sender: envelope.sender,
24
24
  });
25
- this.phase = 'Evaluation';
25
+ // RFC-MACP-0007 §5 rule 6: a runtime MUST reject any Proposal/
26
+ // Evaluation/Objection after the first accepted Vote. A replayed/
27
+ // retried Proposal (same proposalId, a distinct messageId that still
28
+ // passes applyEnvelope's message_id dedup gate) must therefore not
29
+ // rewind `phase` from 'Voting' back to 'Evaluation' (issue #153).
30
+ // Guarding on the initial phase value — mirrors macp-sdk-python's
31
+ // identical `if self.phase == "Proposal": self._set_phase(...)`.
32
+ if (this.phase === 'Proposal')
33
+ this.setPhase('Evaluation');
26
34
  break;
27
35
  }
28
36
  case 'Evaluation': {
@@ -80,20 +88,19 @@ class DecisionProjection extends base_1.BaseProjection {
80
88
  // runtime rejects any session-scoped message once the session is
81
89
  // non-OPEN, so a `Vote` cannot legally follow a `Commitment` in
82
90
  // accepted history. If one reaches here anyway (the caller violated
83
- // the accepted-only contract — see `applyEnvelope`'s docblock), do
84
- // not regress `phase` out of `'Committed'`. Note "phase" itself is
85
- // not a normative MACP term (it appears once, in passing, at
86
- // RFC-MACP-0012 `:211`); this guard is justified by session
87
- // terminality, not by a phase specification. An anomaly would be
88
- // recorded here too, but unlike five of the six sites that were
89
- // "frozen pending cross-SDK agreement" (issue #126/#128, now
90
- // resolved for `duplicate_task_accept`/`settled_handoff` — see
91
- // `base.ts`), this one is a session-terminality phase-regression
92
- // guard, not a settlement discard, and remains genuinely undecided
93
- // per #128's own explicit scoping. No behavior change here.
94
- if (this.phase !== 'Committed') {
95
- this.phase = 'Voting';
96
- }
91
+ // the accepted-only contract — see `applyEnvelope`'s docblock),
92
+ // `setPhase` (see `base.ts`) is what keeps `phase` from regressing
93
+ // out of `'Committed'`. Note "phase" itself is not a normative MACP
94
+ // term (it appears once, in passing, at RFC-MACP-0012 `:211`); this
95
+ // guard is justified by session terminality, not by a phase
96
+ // specification. An anomaly would be recorded here too, but unlike
97
+ // five of the six sites that were "frozen pending cross-SDK
98
+ // agreement" (issue #126/#128, now resolved for
99
+ // `duplicate_task_accept`/`settled_handoff` — see `base.ts`), this
100
+ // one is a session-terminality phase-regression guard, not a
101
+ // settlement discard, and remains genuinely undecided per #128's own
102
+ // explicit scoping. No behavior change here.
103
+ this.setPhase('Voting');
97
104
  break;
98
105
  }
99
106
  default:
@@ -21,7 +21,7 @@ class HandoffProjection extends base_1.BaseProjection {
21
21
  sender: envelope.sender,
22
22
  status: 'offered',
23
23
  });
24
- this.phase = 'OfferPending';
24
+ this.setPhase('OfferPending');
25
25
  break;
26
26
  }
27
27
  case 'HandoffContext': {
@@ -40,7 +40,7 @@ class HandoffProjection extends base_1.BaseProjection {
40
40
  }
41
41
  handoff.contextContentType = record.contentType;
42
42
  if (this.phase === 'OfferPending')
43
- this.phase = 'ContextSharing';
43
+ this.setPhase('ContextSharing');
44
44
  }
45
45
  break;
46
46
  }
@@ -72,7 +72,7 @@ class HandoffProjection extends base_1.BaseProjection {
72
72
  // (proto-registry), so this is always a real boolean once proto 0.1.6
73
73
  // is loaded — `true` marks a runtime synthetic implicit accept.
74
74
  handoff.implicit = record.implicit ?? false;
75
- this.phase = 'Accepted';
75
+ this.setPhase('Accepted');
76
76
  }
77
77
  else {
78
78
  this.recordAnomaly({
@@ -107,7 +107,7 @@ class HandoffProjection extends base_1.BaseProjection {
107
107
  if (handoff.status === 'offered' || handoff.status === 'context_sent') {
108
108
  handoff.status = 'declined';
109
109
  handoff.declinedBy = record.declinedBy;
110
- this.phase = 'Declined';
110
+ this.setPhase('Declined');
111
111
  }
112
112
  else {
113
113
  this.recordAnomaly({
@@ -38,6 +38,19 @@ export interface ProposalRejectRecord {
38
38
  reason?: string;
39
39
  sender: string;
40
40
  }
41
+ /**
42
+ * The three reachable `ProposalRecord.status` values, as a runtime array —
43
+ * cross-SDK contract pinned by the spec repo's `schemas/parity/contract.json`
44
+ * (`proposal_disposition.projection_status_values`, `contract_version`
45
+ * 1.3.0, issue #156). `satisfies readonly ProposalRecord['status'][]` links
46
+ * this array to the type above at compile time, and `_ProposalStatusIsFrozen`
47
+ * below closes the loop in the other direction — together they prove at
48
+ * `npm run check` time, not just in a docblock, that `status` can never hold
49
+ * an acceptance-shaped value like `'accepted'` (see `ProposalRecord.status`'s
50
+ * own docblock above for why acceptance is tracked separately). See
51
+ * `tests/parity/contract.test.ts`.
52
+ */
53
+ export declare const PROPOSAL_STATUS_VALUES: readonly ["open", "rejected", "withdrawn"];
41
54
  export declare class ProposalProjection extends BaseProjection {
42
55
  protected readonly mode = "macp.mode.proposal.v1";
43
56
  readonly proposals: Map<string, ProposalRecord>;
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProposalProjection = void 0;
3
+ exports.ProposalProjection = exports.PROPOSAL_STATUS_VALUES = void 0;
4
4
  const constants_1 = require("../constants");
5
5
  const base_1 = require("./base");
6
+ /**
7
+ * The three reachable `ProposalRecord.status` values, as a runtime array —
8
+ * cross-SDK contract pinned by the spec repo's `schemas/parity/contract.json`
9
+ * (`proposal_disposition.projection_status_values`, `contract_version`
10
+ * 1.3.0, issue #156). `satisfies readonly ProposalRecord['status'][]` links
11
+ * this array to the type above at compile time, and `_ProposalStatusIsFrozen`
12
+ * below closes the loop in the other direction — together they prove at
13
+ * `npm run check` time, not just in a docblock, that `status` can never hold
14
+ * an acceptance-shaped value like `'accepted'` (see `ProposalRecord.status`'s
15
+ * own docblock above for why acceptance is tracked separately). See
16
+ * `tests/parity/contract.test.ts`.
17
+ */
18
+ exports.PROPOSAL_STATUS_VALUES = [
19
+ 'open',
20
+ 'rejected',
21
+ 'withdrawn',
22
+ ];
6
23
  class ProposalProjection extends base_1.BaseProjection {
7
24
  mode = constants_1.MODE_PROPOSAL;
8
25
  proposals = new Map();
@@ -86,7 +103,7 @@ class ProposalProjection extends base_1.BaseProjection {
86
103
  const proposal = this.proposals.get(record.proposalId);
87
104
  if (proposal) {
88
105
  proposal.status = 'rejected';
89
- this.phase = 'TerminalRejected';
106
+ this.setPhase('TerminalRejected');
90
107
  }
91
108
  }
92
109
  break;
@@ -15,7 +15,7 @@ class QuorumProjection extends base_1.BaseProjection {
15
15
  case 'ApprovalRequest': {
16
16
  const record = payload;
17
17
  this.requests.set(record.requestId, { ...record, sender: envelope.sender });
18
- this.phase = 'Voting';
18
+ this.setPhase('Voting');
19
19
  break;
20
20
  }
21
21
  case 'Approve': {
@@ -33,12 +33,19 @@ export interface TaskFailRecord {
33
33
  retryable: boolean;
34
34
  sender: string;
35
35
  }
36
+ export interface TaskRejectRecord {
37
+ taskId: string;
38
+ assignee: string;
39
+ reason?: string;
40
+ sender: string;
41
+ }
36
42
  export declare class TaskProjection extends BaseProjection {
37
43
  protected readonly mode = "macp.mode.task.v1";
38
44
  readonly tasks: Map<string, TaskRecord>;
39
45
  readonly updates: TaskUpdateRecord[];
40
46
  readonly completions: TaskCompleteRecord[];
41
47
  readonly failures: TaskFailRecord[];
48
+ readonly rejections: TaskRejectRecord[];
42
49
  phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
43
50
  /**
44
51
  * The **session-level** active assignment: who currently holds the session's