macp-sdk-typescript 0.13.0 → 0.14.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 +48 -0
- package/dist/agent/runner.d.ts +23 -1
- package/dist/agent/runner.js +57 -8
- package/dist/projections/base.d.ts +6 -3
- package/dist/projections/proposal.d.ts +18 -1
- package/dist/projections/quorum.js +18 -15
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project uses [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.14.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.13.0...v0.14.0) (2026-10-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* **agent:** encodeExtensions() (src/agent/runner.ts) no longer JSON-stringifies a session_start.extensions value. A string value is now base64-decoded (falling back to raw UTF-8 if it isn't valid base64); a number, boolean, plain object, array, or null value now throws instead of being silently JSON-encoded, as does a present-but-non-object extensions map itself. Buffer/Uint8Array values are unaffected. This reverses the UTF-8-JSON-encoding behavior documented in the 0.2.3 (SDK-TS-1) CHANGELOG entry, which silently put wire bytes on the connection that a conforming peer cannot decode.
|
|
13
|
+
* **projections:** ProposalRecord.status's type no longer accepts 'accepted'. A TypeScript consumer comparing status === 'accepted' or switching on it now gets a compile-time TS2367/TS2678/TS2322 instead of silently-dead code; a plain-JS (or `as any`-cast) caller sees no behavior change, since the comparison always evaluated false.
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **agent:** encodeExtensions() base64-decodes extensions values ([#149](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/149)) ([72c6ea4](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/72c6ea484e1dd5e75dbfc0bd12cbed787eeeacbd))
|
|
18
|
+
* **projections:** ProposalRecord.status no longer accepts 'accepted' ([#147](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/147)) ([c5142d7](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/c5142d7c1ccd37b128549e512e9d5c6436899e30))
|
|
19
|
+
|
|
7
20
|
## [0.13.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.12.0...v0.13.0) (2026-09-30)
|
|
8
21
|
|
|
9
22
|
|
|
@@ -131,6 +144,30 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
131
144
|
still only fails server-side). A caller that genuinely needs one
|
|
132
145
|
credential reused across multiple senders should use
|
|
133
146
|
`Auth.bearer(token, senderHint)` (the legacy two-arg form) instead.
|
|
147
|
+
* **projections:** `ProposalRecord.status`'s type no longer accepts
|
|
148
|
+
`'accepted'` (issue #146). No code path has ever assigned it — acceptance
|
|
149
|
+
is tracked separately via `accepts`/`isAccepted()`, since it's a
|
|
150
|
+
per-sender, supersedable relation (RFC-MACP-0008 §5 rule 5), not a
|
|
151
|
+
per-proposal fact a scalar field could hold. A TypeScript consumer
|
|
152
|
+
comparing `status === 'accepted'` or switching on it now gets a
|
|
153
|
+
compile-time `TS2367`/`TS2678`/`TS2322` instead of silently-dead code; a
|
|
154
|
+
plain-JS (or `as any`-cast) caller sees no behavior change, since the
|
|
155
|
+
comparison always evaluated `false`.
|
|
156
|
+
* **agent:** `fromBootstrap()`'s `encodeExtensions()` now base64-decodes a
|
|
157
|
+
`session_start.extensions` string value (falling back to raw UTF-8 for a
|
|
158
|
+
non-base64 string), instead of `JSON.stringify`-ing it, per RFC-MACP-0001
|
|
159
|
+
§10.3 (a protobuf `bytes` field's canonical JSON representation is
|
|
160
|
+
base64) — the wire-level `extensions` map is `map<string, bytes>` (issue
|
|
161
|
+
#139). This reverses the UTF-8-JSON-encoding behavior documented at the
|
|
162
|
+
`0.2.3` entry above (SDK-TS-1): that release's choice silently put wire
|
|
163
|
+
bytes on the connection that a conforming peer (including this SDK's own
|
|
164
|
+
`macp-sdk-python` counterpart) cannot decode, even for the canonical
|
|
165
|
+
example value. A number, boolean, plain object, array, or `null`
|
|
166
|
+
extension value — previously silently JSON-encoded — now throws at
|
|
167
|
+
bootstrap construction instead, naming the offending key; so does a
|
|
168
|
+
present-but-non-object `extensions` map itself. `Buffer`/`Uint8Array`
|
|
169
|
+
values are unaffected (same passthrough as before, for TS-only
|
|
170
|
+
in-process callers — never reachable via a JSON bootstrap file).
|
|
134
171
|
|
|
135
172
|
### Features
|
|
136
173
|
|
|
@@ -191,6 +228,12 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
191
228
|
sites. `DecisionProjection`'s late-`Vote`-after-`Commitment` site remains
|
|
192
229
|
the one still-open, undecided case (per #128's own explicit scoping) —
|
|
193
230
|
unaffected by this change.
|
|
231
|
+
- **`encodeExtensions()` is now exported from `macp-sdk-typescript/agent`**
|
|
232
|
+
(issue #139), so a caller constructing a bootstrap `extensions` map
|
|
233
|
+
programmatically can validate/encode a value the same way
|
|
234
|
+
`fromBootstrap()` does, and so it's directly unit-testable (a
|
|
235
|
+
`Buffer`/`Uint8Array` value can't round-trip through a JSON bootstrap
|
|
236
|
+
file otherwise).
|
|
194
237
|
|
|
195
238
|
### Changed
|
|
196
239
|
|
|
@@ -466,6 +509,11 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
466
509
|
site — `taskId`, `title`, `requestId`, `proposalId`, `option`, `handoffId`,
|
|
467
510
|
`targetParticipant`, `modeVersion`, `configurationVersion` — is unaffected
|
|
468
511
|
and still rejects an empty value.
|
|
512
|
+
- **`ProposalRecord.status`'s declared type no longer lies about
|
|
513
|
+
`'accepted'`** (issue #146). The union included `'accepted'` since this
|
|
514
|
+
type was first written, but nothing has ever assigned it — see the
|
|
515
|
+
`### ⚠ BREAKING CHANGES` entry above for why, and why that correction is a
|
|
516
|
+
breaking (not purely additive) change.
|
|
469
517
|
|
|
470
518
|
### Documentation
|
|
471
519
|
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface BootstrapPayload {
|
|
|
24
24
|
policy_version?: string;
|
|
25
25
|
context?: Record<string, unknown>;
|
|
26
26
|
context_id?: string;
|
|
27
|
-
extensions?: Record<string,
|
|
27
|
+
extensions?: Record<string, string | Buffer | Uint8Array>;
|
|
28
28
|
roots?: Array<{
|
|
29
29
|
uri: string;
|
|
30
30
|
name?: string;
|
|
@@ -50,3 +50,25 @@ export interface BootstrapPayload {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
export declare function fromBootstrap(bootstrapPath?: string): Participant;
|
|
53
|
+
/**
|
|
54
|
+
* Coerce a bootstrap `session_start.extensions` map into the
|
|
55
|
+
* `Record<string, Buffer>` form the SessionStart envelope expects
|
|
56
|
+
* (`map<string, bytes>` on the wire).
|
|
57
|
+
*
|
|
58
|
+
* Per RFC-MACP-0001 §10.3, a protobuf `bytes` field's canonical JSON
|
|
59
|
+
* representation is a base64 string, so a `string` value is base64-decoded
|
|
60
|
+
* first (strict shape check via `BASE64_PATTERN`, not Node's lenient
|
|
61
|
+
* decoder); a string that isn't valid base64 falls back to raw UTF-8 bytes.
|
|
62
|
+
* This base64-first heuristic — and the resulting ambiguity for a plain
|
|
63
|
+
* string that happens to also be valid base64 — mirrors
|
|
64
|
+
* `macp-sdk-python`'s `_decode_extensions` on purpose, to stay
|
|
65
|
+
* byte-for-byte interoperable; it is a known, tracked wart, not fixed here.
|
|
66
|
+
* `Buffer`/`Uint8Array` values pass through unchanged, for TS-only
|
|
67
|
+
* in-process callers that already hold raw bytes (never reachable via a
|
|
68
|
+
* JSON bootstrap file). Anything else — including the `extensions`
|
|
69
|
+
* container itself being a non-object (string/number/boolean/array) —
|
|
70
|
+
* throws, naming the offending key and/or type, so a malformed bootstrap
|
|
71
|
+
* fails loudly at construction instead of silently corrupting wire bytes a
|
|
72
|
+
* peer can't decode (issue #139).
|
|
73
|
+
*/
|
|
74
|
+
export declare function encodeExtensions(extensions: Record<string, string | Buffer | Uint8Array> | undefined): Record<string, Buffer> | undefined;
|
package/dist/agent/runner.js
CHANGED
|
@@ -34,11 +34,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.fromBootstrap = fromBootstrap;
|
|
37
|
+
exports.encodeExtensions = encodeExtensions;
|
|
37
38
|
const fs = __importStar(require("node:fs"));
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
39
40
|
const auth_1 = require("../auth");
|
|
40
41
|
const client_1 = require("../client");
|
|
41
42
|
const constants_1 = require("../constants");
|
|
43
|
+
const logging_1 = require("../logging");
|
|
42
44
|
const participant_1 = require("./participant");
|
|
43
45
|
function fromBootstrap(bootstrapPath) {
|
|
44
46
|
const resolvedPath = bootstrapPath ?? process.env.MACP_BOOTSTRAP_FILE;
|
|
@@ -111,17 +113,53 @@ function fromBootstrap(bootstrapPath) {
|
|
|
111
113
|
};
|
|
112
114
|
return new participant_1.Participant(config);
|
|
113
115
|
}
|
|
116
|
+
// Matches a syntactically valid base64 string (correct alphabet, correct
|
|
117
|
+
// padding) — a cheap stand-in for Python's `base64.b64decode(value,
|
|
118
|
+
// validate=True)` so a plainly non-base64 string (e.g. "pack:example-001")
|
|
119
|
+
// is rejected up front rather than silently mis-decoded by Node's lenient
|
|
120
|
+
// `Buffer.from(str, 'base64')`, which ignores invalid characters instead of
|
|
121
|
+
// raising. Matches the empty string too (decodes to an empty buffer either
|
|
122
|
+
// way, so which branch handles it is immaterial).
|
|
123
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
124
|
+
function tryDecodeBase64(value) {
|
|
125
|
+
if (!BASE64_PATTERN.test(value))
|
|
126
|
+
return undefined;
|
|
127
|
+
return Buffer.from(value, 'base64');
|
|
128
|
+
}
|
|
129
|
+
function describeExtensionsValueType(value) {
|
|
130
|
+
if (value === null)
|
|
131
|
+
return 'null';
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return 'array';
|
|
134
|
+
return typeof value;
|
|
135
|
+
}
|
|
114
136
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
137
|
+
* Coerce a bootstrap `session_start.extensions` map into the
|
|
138
|
+
* `Record<string, Buffer>` form the SessionStart envelope expects
|
|
139
|
+
* (`map<string, bytes>` on the wire).
|
|
140
|
+
*
|
|
141
|
+
* Per RFC-MACP-0001 §10.3, a protobuf `bytes` field's canonical JSON
|
|
142
|
+
* representation is a base64 string, so a `string` value is base64-decoded
|
|
143
|
+
* first (strict shape check via `BASE64_PATTERN`, not Node's lenient
|
|
144
|
+
* decoder); a string that isn't valid base64 falls back to raw UTF-8 bytes.
|
|
145
|
+
* This base64-first heuristic — and the resulting ambiguity for a plain
|
|
146
|
+
* string that happens to also be valid base64 — mirrors
|
|
147
|
+
* `macp-sdk-python`'s `_decode_extensions` on purpose, to stay
|
|
148
|
+
* byte-for-byte interoperable; it is a known, tracked wart, not fixed here.
|
|
149
|
+
* `Buffer`/`Uint8Array` values pass through unchanged, for TS-only
|
|
150
|
+
* in-process callers that already hold raw bytes (never reachable via a
|
|
151
|
+
* JSON bootstrap file). Anything else — including the `extensions`
|
|
152
|
+
* container itself being a non-object (string/number/boolean/array) —
|
|
153
|
+
* throws, naming the offending key and/or type, so a malformed bootstrap
|
|
154
|
+
* fails loudly at construction instead of silently corrupting wire bytes a
|
|
155
|
+
* peer can't decode (issue #139).
|
|
121
156
|
*/
|
|
122
157
|
function encodeExtensions(extensions) {
|
|
123
|
-
if (
|
|
158
|
+
if (extensions === undefined || extensions === null)
|
|
124
159
|
return undefined;
|
|
160
|
+
if (typeof extensions !== 'object' || Array.isArray(extensions)) {
|
|
161
|
+
throw new Error(`bootstrap session_start.extensions must be an object, got ${describeExtensionsValueType(extensions)}`);
|
|
162
|
+
}
|
|
125
163
|
const out = {};
|
|
126
164
|
for (const [key, value] of Object.entries(extensions)) {
|
|
127
165
|
if (Buffer.isBuffer(value)) {
|
|
@@ -130,8 +168,19 @@ function encodeExtensions(extensions) {
|
|
|
130
168
|
else if (value instanceof Uint8Array) {
|
|
131
169
|
out[key] = Buffer.from(value);
|
|
132
170
|
}
|
|
171
|
+
else if (typeof value === 'string') {
|
|
172
|
+
const decoded = tryDecodeBase64(value);
|
|
173
|
+
if (decoded !== undefined) {
|
|
174
|
+
logging_1.logger.debug(`[fromBootstrap] extensions["${key}"] decoded as base64`);
|
|
175
|
+
out[key] = decoded;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
logging_1.logger.debug(`[fromBootstrap] extensions["${key}"] is not valid base64; falling back to raw UTF-8`);
|
|
179
|
+
out[key] = Buffer.from(value, 'utf8');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
133
182
|
else {
|
|
134
|
-
|
|
183
|
+
throw new Error(`bootstrap extensions["${key}"] must be a base64-encoded string (or Buffer/Uint8Array), got ${describeExtensionsValueType(value)}`);
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
return out;
|
|
@@ -14,9 +14,12 @@ export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot' | 'dup
|
|
|
14
14
|
* this proposal (RFC-MACP-0007 §5.3), or a second distinct ballot across
|
|
15
15
|
* `Approve`/`Reject`/`Abstain` from this sender for this request
|
|
16
16
|
* (RFC-MACP-0011 §5 rule 3), was observed and discarded — the first stands.
|
|
17
|
-
* (RFC-MACP-0011 §5 rule 3
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* (RFC-MACP-0011 §5 rule 3 states "the first accepted ballot stands"
|
|
18
|
+
* directly, since the rule-3 hardening in spec PR
|
|
19
|
+
* multiagentcoordinationprotocol#85 — before that, this SDK's
|
|
20
|
+
* first-ballot-wins behaviour was an inference from parity with
|
|
21
|
+
* RFC-MACP-0007 §5.3 plus runtime-enforced behaviour, not a direct
|
|
22
|
+
* RFC-0011 citation.)
|
|
20
23
|
*
|
|
21
24
|
* **Deliberately narrow claim — do not overclaim "this transcript violates
|
|
22
25
|
* the spec".** A projection cannot tell a genuinely non-conforming source
|
|
@@ -8,7 +8,24 @@ export interface ProposalRecord {
|
|
|
8
8
|
tags?: string[];
|
|
9
9
|
sender: string;
|
|
10
10
|
supersedes?: string;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Reachable values only (issue #146). An `Accept` is recorded on the
|
|
13
|
+
* projection's `accepts` list (and surfaced via `acceptedProposal()` /
|
|
14
|
+
* `isAccepted()`), never on this field — no code path assigns `'accepted'`
|
|
15
|
+
* here. A non-terminal `Reject` likewise leaves this `'open'`; only a
|
|
16
|
+
* terminal `Reject` sets `'rejected'`.
|
|
17
|
+
*
|
|
18
|
+
* That is by design, not an omission: acceptance is a per-sender,
|
|
19
|
+
* supersedable relation (RFC-MACP-0008 §5 rule 5), not a per-proposal
|
|
20
|
+
* fact, so a scalar field here cannot hold it — "alice accepts p2 while
|
|
21
|
+
* bob still accepts p1" is a legal state. This mirrors the runtime, whose
|
|
22
|
+
* `ProposalDisposition` is `{Live, Withdrawn}` with acceptance in a
|
|
23
|
+
* separate `accepts` map, and `macp-sdk-python`'s own `status: str`
|
|
24
|
+
* (documented the same way after its issue #112). `task.ts`/`handoff.ts`
|
|
25
|
+
* do set `'accepted'`-equivalent statuses because their acceptance is one
|
|
26
|
+
* actor claiming one slot — this mode's isn't. See `docs/modes/proposal.md`.
|
|
27
|
+
*/
|
|
28
|
+
status: 'open' | 'rejected' | 'withdrawn';
|
|
12
29
|
}
|
|
13
30
|
export interface ProposalAcceptRecord {
|
|
14
31
|
proposalId: string;
|
|
@@ -54,21 +54,24 @@ class QuorumProjection extends base_1.BaseProjection {
|
|
|
54
54
|
const senderMap = this.ballots.get(requestId) ?? new Map();
|
|
55
55
|
const kept = senderMap.get(envelope.sender);
|
|
56
56
|
if (kept !== undefined) {
|
|
57
|
-
// RFC-MACP-0011 §5 rule 3 (`:
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// not a change of vote, so this guard is keyed on the
|
|
66
|
-
// the request — never on sender + vote.
|
|
67
|
-
// identically in all three arms
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
57
|
+
// RFC-MACP-0011 §5 rule 3 (`:69`, hardened by spec PR
|
|
58
|
+
// multiagentcoordinationprotocol#85): "Each eligible participant MUST
|
|
59
|
+
// cast at most one ballot per request_id... A runtime MUST reject a
|
|
60
|
+
// second ballot from the same sender for the same request_id,
|
|
61
|
+
// regardless of the type of either ballot; the first accepted ballot
|
|
62
|
+
// stands." Participation itself is still MAY (a participant need not
|
|
63
|
+
// ballot at all) — only the one-per-participant cap and first-wins
|
|
64
|
+
// outcome are MUST. A later ballot of a *different* type is still a
|
|
65
|
+
// duplicate, not a change of vote, so this guard is keyed on the
|
|
66
|
+
// sender ALONE within the request — never on sender + vote.
|
|
67
|
+
// macp-runtime enforces this identically in all three arms
|
|
68
|
+
// (quorum.rs:164/184/204).
|
|
69
|
+
//
|
|
70
|
+
// Keeping the sender's FIRST ballot is now a direct rule-3 citation
|
|
71
|
+
// (above), not an inference — before PR #85 hardened the rule, this
|
|
72
|
+
// comment relied on parity with RFC-MACP-0007 §5 item 3's first-stands
|
|
73
|
+
// rule for `Vote` plus macp-runtime's observed behavior, since RFC-0011
|
|
74
|
+
// capped *how many* ballots without yet stating *which* stands.
|
|
72
75
|
this.recordAnomaly({
|
|
73
76
|
kind: 'duplicate_ballot',
|
|
74
77
|
mode: envelope.mode,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED