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/CHANGELOG.md +77 -0
- package/README.md +50 -20
- package/dist/client.d.ts +18 -5
- package/dist/client.js +24 -10
- package/dist/commitment-hash.d.ts +26 -0
- package/dist/commitment-hash.js +180 -0
- package/dist/constants.d.ts +29 -0
- package/dist/constants.js +50 -1
- package/dist/decision.js +4 -1
- package/dist/errors.d.ts +67 -6
- package/dist/errors.js +90 -47
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/policy.js +7 -3
- package/dist/projections/base.d.ts +14 -0
- package/dist/projections/base.js +18 -0
- package/dist/projections/decision.js +22 -15
- package/dist/projections/handoff.js +4 -4
- package/dist/projections/proposal.d.ts +13 -0
- package/dist/projections/proposal.js +19 -2
- package/dist/projections/quorum.js +1 -1
- package/dist/projections/task.d.ts +7 -0
- package/dist/projections/task.js +15 -4
- package/dist/types.d.ts +1 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +6 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watchers.js +8 -6
- package/package.json +2 -2
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
|
-
|
|
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)
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
|
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)
|
|
18
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
49
|
-
// First try:
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
53
|
-
// Second try:
|
|
54
|
-
|
|
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
|
|
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)
|
|
109
|
-
//
|
|
110
|
-
|
|
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
|
}
|
package/dist/projections/base.js
CHANGED
|
@@ -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
|
-
|
|
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),
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// RFC-MACP-0012 `:211`); this
|
|
87
|
-
// terminality, not by a phase
|
|
88
|
-
// recorded here too, but unlike
|
|
89
|
-
// "frozen pending cross-SDK
|
|
90
|
-
//
|
|
91
|
-
// `base.ts`), this
|
|
92
|
-
//
|
|
93
|
-
// per #128's own
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|