@ziffer-io/mcp 0.1.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/LICENSE +120 -0
- package/README.md +136 -0
- package/THIRD-PARTY-NOTICES +610 -0
- package/dist/anchor.d.ts +76 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +161 -0
- package/dist/anchor.js.map +1 -0
- package/dist/bin.d.ts +24 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +56 -0
- package/dist/bin.js.map +1 -0
- package/dist/client.d.ts +37 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +41 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +134 -0
- package/dist/config.js.map +1 -0
- package/dist/generated/guide-source.d.ts +3 -0
- package/dist/generated/guide-source.d.ts.map +1 -0
- package/dist/generated/guide-source.js +6 -0
- package/dist/generated/guide-source.js.map +1 -0
- package/dist/guide.d.ts +45 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +81 -0
- package/dist/guide.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +147 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +226 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +330 -0
- package/dist/tools.js.map +1 -0
- package/package.json +64 -0
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five tools, as plain functions (ACP-197 section 6b point 2; ACP-213
|
|
3
|
+
* section 9.2 point 3 added the fifth).
|
|
4
|
+
*
|
|
5
|
+
* Every handler here takes its dependencies as arguments and returns a
|
|
6
|
+
* {@link ToolOutcome}. Nothing in this file imports the MCP SDK: `server.ts`
|
|
7
|
+
* adapts these to the protocol's content shape. That seam exists so the tests
|
|
8
|
+
* exercise the behaviour rather than the framing, and so the answer to "what
|
|
9
|
+
* does `propose` do when the key is unset" is one function call.
|
|
10
|
+
*
|
|
11
|
+
* # What is deliberately absent
|
|
12
|
+
*
|
|
13
|
+
* There is no approve tool, no simulated approval, and no tool that writes a
|
|
14
|
+
* file. The reason for the first two is worth carrying here rather than leaving
|
|
15
|
+
* in a ticket: **a simulated approval minted on a developer's laptop is a fake
|
|
16
|
+
* receipt factory**, and a receipt factory is the exact artifact this product
|
|
17
|
+
* exists to make impossible.
|
|
18
|
+
*
|
|
19
|
+
* Until ACP-213 this file also said there was no sandbox tool, and that a
|
|
20
|
+
* sandbox mode was "a follow-up with its own design". That follow-up landed and
|
|
21
|
+
* the design is the reason the sentence could change: a Ziffer sandbox is a
|
|
22
|
+
* SEPARATE TENANT with its own receipt signing identity, approved by a robot
|
|
23
|
+
* that walks the real quorum path with keys enrolled only in sandbox bundles
|
|
24
|
+
* (`services/sandbox-approver`, `docs/onboarding/sandbox.md`). Nothing about it
|
|
25
|
+
* is client-side, which is why {@link sandboxStatus} only ever REPORTS: it
|
|
26
|
+
* reads a name and asks the API about one decision. An approval on this side of
|
|
27
|
+
* the wire would still be a forgery, and there is still no tool for it.
|
|
28
|
+
*
|
|
29
|
+
* The third is a boundary rather than a danger: this server has no hands on the
|
|
30
|
+
* developer's machine. It serves knowledge and Ziffer-side calls; the coding
|
|
31
|
+
* agent talking to it is the thing that edits code. A file-writing tool here
|
|
32
|
+
* would make this process an editor with none of an editor's review surface.
|
|
33
|
+
*/
|
|
34
|
+
import { canon } from '@ziffer-io/verify';
|
|
35
|
+
import { type Env } from './config.js';
|
|
36
|
+
/**
|
|
37
|
+
* What a tool hands back.
|
|
38
|
+
*
|
|
39
|
+
* `isError` is the MCP protocol's flag for "this call did not do what it was
|
|
40
|
+
* asked", and it is set for every refusal — a configuration error, a gateway
|
|
41
|
+
* refusal, a receipt that does not verify. It is deliberately NOT set for a
|
|
42
|
+
* decision whose outcome is `DENY`: the tool was asked to get a decision and it
|
|
43
|
+
* got one. Marking a successful denial as a tool error teaches an agent to
|
|
44
|
+
* treat "the policy said no" as a malfunction to route around, which is the
|
|
45
|
+
* behaviour this whole system exists to prevent.
|
|
46
|
+
*/
|
|
47
|
+
export interface ToolOutcome {
|
|
48
|
+
readonly text: string;
|
|
49
|
+
readonly isError: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The §1 response to `POST /v1/proposals`, as the gateway sends it.
|
|
53
|
+
*
|
|
54
|
+
* Field names are §1's, in its spelling, because the tools return the response
|
|
55
|
+
* VERBATIM. A DTO with prettier names here would mean the agent reading our
|
|
56
|
+
* `docs/onboarding/sdk.md` sees one set of field names in the HTTP examples and
|
|
57
|
+
* a different set coming out of the tools, and would have to be told they are
|
|
58
|
+
* the same object.
|
|
59
|
+
*/
|
|
60
|
+
export interface SubmittedDecision {
|
|
61
|
+
readonly decision_id: string;
|
|
62
|
+
readonly status: 'pending' | 'decided';
|
|
63
|
+
readonly outcome?: string;
|
|
64
|
+
readonly clause?: string;
|
|
65
|
+
}
|
|
66
|
+
/** The §1 response to `GET /v1/decisions/{id}`: the above, plus the receipt
|
|
67
|
+
* when a signed one exists. `receipt` is `unknown` because this package never
|
|
68
|
+
* looks inside it — it is served to the agent as it arrived and handed to
|
|
69
|
+
* `@ziffer-io/verify` unparsed by anything of ours. */
|
|
70
|
+
export interface DecisionRecord extends SubmittedDecision {
|
|
71
|
+
readonly receipt?: unknown;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The client surface, fixed by ACP-197 section 6, narrowed to what these tools
|
|
75
|
+
* call.
|
|
76
|
+
*
|
|
77
|
+
* Declared as an interface rather than imported as a class so that `tools.ts`
|
|
78
|
+
* has no dependency on the transport, and so the tests can drive every §1
|
|
79
|
+
* response shape — including the ones a stub HTTP server makes awkward to
|
|
80
|
+
* produce, like a gateway that is unreachable. `client.ts` is the one file that
|
|
81
|
+
* knows the concrete `ZifferClient`, and it is the one place a difference
|
|
82
|
+
* between §6's spelling and §1's would be reconciled.
|
|
83
|
+
*/
|
|
84
|
+
export interface DecisionClient {
|
|
85
|
+
propose(proposal: unknown): Promise<SubmittedDecision>;
|
|
86
|
+
decision(decisionId: string): Promise<DecisionRecord>;
|
|
87
|
+
}
|
|
88
|
+
/** How a tool gets a client, given the environment. Async and fallible: it
|
|
89
|
+
* resolves configuration, so an unset `ZIFFER_API_KEY` surfaces here. */
|
|
90
|
+
export type ClientFactory = (env: Env) => Promise<DecisionClient>;
|
|
91
|
+
/**
|
|
92
|
+
* `propose` — submit one wire Proposal and return the §1 response verbatim.
|
|
93
|
+
*
|
|
94
|
+
* The proposal is passed through untouched. This server does not fill in a
|
|
95
|
+
* `tenant_id`, and it must not: ACP-197 section 1 makes the KEY the tenant, and
|
|
96
|
+
* a body whose `tenant_id` disagrees is refused rather than rewritten, because
|
|
97
|
+
* the proposal is signed material downstream and a rewriter would have become
|
|
98
|
+
* its author. Helpfully "correcting" the field here would produce exactly the
|
|
99
|
+
* silent authorship the gateway's refusal exists to prevent.
|
|
100
|
+
*/
|
|
101
|
+
export declare function propose(clientFor: ClientFactory, env: Env, proposal: unknown): Promise<ToolOutcome>;
|
|
102
|
+
/**
|
|
103
|
+
* `check_decision` — one decision by id, receipt included when present.
|
|
104
|
+
*
|
|
105
|
+
* The receipt is re-serialised as part of the response object rather than
|
|
106
|
+
* spliced in as text. That is safe HERE, and only here, because this output is
|
|
107
|
+
* for an agent to read: the copy that gets VERIFIED is the one
|
|
108
|
+
* `explain_receipt` is handed, and the hash it checks is recomputed from the
|
|
109
|
+
* proposal bytes, never from a re-encoding of the receipt. Nothing downstream
|
|
110
|
+
* of this tool treats its text as signed material.
|
|
111
|
+
*/
|
|
112
|
+
export declare function checkDecision(clientFor: ClientFactory, env: Env, decisionId: string): Promise<ToolOutcome>;
|
|
113
|
+
/**
|
|
114
|
+
* `get_integration_guide` — `docs/onboarding/sdk.md`, for one language.
|
|
115
|
+
*
|
|
116
|
+
* The one tool that needs no configuration, deliberately: an agent should be
|
|
117
|
+
* able to read how to integrate before any credential exists. That is also why
|
|
118
|
+
* `config.ts` lets the server start unconfigured — a server that refused to
|
|
119
|
+
* boot without an API key could not answer the question a developer asks first.
|
|
120
|
+
*/
|
|
121
|
+
export declare function getIntegrationGuide(language: string): ToolOutcome;
|
|
122
|
+
/**
|
|
123
|
+
* The marker's naming half (ACP-213). One spelling, and it is not the one that
|
|
124
|
+
* decides: `services/sandbox-approver/src/sandbox.rs` holds the rule that
|
|
125
|
+
* governs, against the tenant the SIGNED bundle names, beside an allowlist of
|
|
126
|
+
* receipt identities no client can see. This copy exists so a developer is told
|
|
127
|
+
* what they are pointed at before they send a proposal, and it says so.
|
|
128
|
+
*/
|
|
129
|
+
export declare const SANDBOX_SUFFIX = "-sandbox";
|
|
130
|
+
/**
|
|
131
|
+
* Is this tenant id spelled as a sandbox?
|
|
132
|
+
*
|
|
133
|
+
* The suffix has to be attached to something: a tenant literally named
|
|
134
|
+
* `-sandbox` would be a tenant with an empty production name, which satisfies
|
|
135
|
+
* the convention while meaning nothing. Same rule, same words, as the Rust
|
|
136
|
+
* predicate and `tools/provision-sandbox.py`.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isSandboxName(tenantId: string): boolean;
|
|
139
|
+
/** What `sandbox_status` is handed. */
|
|
140
|
+
export interface SandboxArgs {
|
|
141
|
+
/** The tenant id the caller's proposals carry. */
|
|
142
|
+
readonly tenantId: string;
|
|
143
|
+
/** A decision to ask the API about, for the approver's liveness. Optional:
|
|
144
|
+
* the naming half is answerable without one, and saying so is better than
|
|
145
|
+
* requiring an id a developer may not have yet. */
|
|
146
|
+
readonly decisionId?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* `sandbox_status` — say whether this tenant is a sandbox, and whether the
|
|
150
|
+
* robot approver has acted on one decision.
|
|
151
|
+
*
|
|
152
|
+
* # What it can establish, and what it cannot
|
|
153
|
+
*
|
|
154
|
+
* It reports the NAMING half of the marker over the tenant id the caller will
|
|
155
|
+
* actually send, and it names the half it cannot see. The binding half — that
|
|
156
|
+
* this tenant's receipt signing identity is registered in the robot's
|
|
157
|
+
* allowlist — is checked inside the deployment, against signed policy, on every
|
|
158
|
+
* `/v1/present`. There is no endpoint that exposes it and this tool does not
|
|
159
|
+
* invent one: a client-side "you are in a sandbox" that rested on nothing would
|
|
160
|
+
* be worse than no answer, because a developer would plan around it.
|
|
161
|
+
*
|
|
162
|
+
* Liveness is the same discipline. There is no health route on the gateway, so
|
|
163
|
+
* the honest signal is one decision's own status. **The signature of a stopped
|
|
164
|
+
* robot is `decided` with `outcome: "ATTEST"` and no receipt** — not `pending`,
|
|
165
|
+
* which is what this said until the 2026-09-03 live run of
|
|
166
|
+
* `tools/rehearse/03-loop.sh` showed a floor-HIGH sandbox proposal answering
|
|
167
|
+
* `decided`/`ATTEST` in the POST itself and never passing through `pending` at
|
|
168
|
+
* all. So the advice that could never fire moved to the branch that sees the
|
|
169
|
+
* real thing. A quorum-awaiting decision keeps that outcome and grows a
|
|
170
|
+
* RECEIPT; a decision that never grows one is the robot being stopped or
|
|
171
|
+
* mis-enrolled, and the text says which of the cases it saw rather than
|
|
172
|
+
* reducing them to a green tick.
|
|
173
|
+
*
|
|
174
|
+
* # Why the tenant is an argument and not configuration
|
|
175
|
+
*
|
|
176
|
+
* Because this server has never held a tenant name and must not start: ACP-197
|
|
177
|
+
* section 1 makes the KEY the tenant, and a `ZIFFER_TENANT` variable would be a
|
|
178
|
+
* second statement of it that could disagree with the key. The tenant id is a
|
|
179
|
+
* value the caller already writes into every proposal — where the gateway binds
|
|
180
|
+
* it, refusing `TenantMismatch` rather than rewriting it — so checking the
|
|
181
|
+
* string the caller is about to send is checking the thing that matters.
|
|
182
|
+
*/
|
|
183
|
+
export declare function sandboxStatus(clientFor: ClientFactory, env: Env, args: SandboxArgs): Promise<ToolOutcome>;
|
|
184
|
+
/** What `explain_receipt` is handed. */
|
|
185
|
+
export interface ExplainArgs {
|
|
186
|
+
/** The receipt, as parsed JSON. Passed to `@ziffer-io/verify` untouched. */
|
|
187
|
+
readonly receipt: unknown;
|
|
188
|
+
/** The EXACT proposal bytes the caller's own code hashed, base64. */
|
|
189
|
+
readonly proposalB64: string;
|
|
190
|
+
/** Overrides `ZIFFER_TRUST_ANCHOR` for this call. */
|
|
191
|
+
readonly trustAnchorPath?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `explain_receipt` — verify a receipt and say `valid` or the named clause.
|
|
195
|
+
*
|
|
196
|
+
* **Zero verification logic of its own.** Every check is `@ziffer-io/verify`'s; this
|
|
197
|
+
* function reads a key file, decodes a base64 argument and formats an answer.
|
|
198
|
+
* A second verifier living in a developer-tools package would be a fourth
|
|
199
|
+
* implementation of §9.3 whose disagreements with the other three would be
|
|
200
|
+
* discovered by a customer.
|
|
201
|
+
*
|
|
202
|
+
* # Why the proposal arrives as bytes and not as an object
|
|
203
|
+
*
|
|
204
|
+
* Because `verifyReceipt` takes bytes, and this tool exists to reproduce the
|
|
205
|
+
* call the developer's own code makes. Accepting an object and encoding it here
|
|
206
|
+
* would put an encoding step between their input and the verifier that their
|
|
207
|
+
* production path does not have, and it would hide one real failure class
|
|
208
|
+
* outright: bytes that are not UTF-8 JSON at all — a truncated read, a
|
|
209
|
+
* double-encoded string, a compressed body — which the verifier refuses under
|
|
210
|
+
* `AT-8a`. Handed an object, this tool could never see that.
|
|
211
|
+
*
|
|
212
|
+
* What it is NOT for is key order. An earlier draft of this file, and of the
|
|
213
|
+
* guide, claimed the caller had to pass `canon(proposal)` and that
|
|
214
|
+
* `JSON.stringify` would produce a spurious `9.3-3`. That is false, and the
|
|
215
|
+
* check that showed it was running all three encodings through this function:
|
|
216
|
+
* `verifyReceipt` PARSES these bytes and canonicalises them itself, because the
|
|
217
|
+
* hash is defined over the canonical encoding rather than over the transport's
|
|
218
|
+
* spacing (`verify.ts` step 3; the Python SDK's `verify` does the same). Any
|
|
219
|
+
* JSON spelling of one object verifies. The hint below therefore names the
|
|
220
|
+
* cause that is real — a different object — rather than the one that reads
|
|
221
|
+
* plausibly.
|
|
222
|
+
*/
|
|
223
|
+
export declare function explainReceipt(env: Env, args: ExplainArgs): Promise<ToolOutcome>;
|
|
224
|
+
/** Re-exported so `server.ts` and the tests name one canonicalisation. */
|
|
225
|
+
export { canon };
|
|
226
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,EAAE,KAAK,EAA0B,MAAM,mBAAmB,CAAC;AAGlE,OAAO,EAA6B,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AAGlE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;uDAGuD;AACvD,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACvD;AAED;yEACyE;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AA6ClE;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,WAAW,CAAC,CAOtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAOtB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAajE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;uDAEmD;IACnD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,WAAW,CAAC,CAuFtB;AAED,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAuDtF;AAED,0EAA0E;AAC1E,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five tools, as plain functions (ACP-197 section 6b point 2; ACP-213
|
|
3
|
+
* section 9.2 point 3 added the fifth).
|
|
4
|
+
*
|
|
5
|
+
* Every handler here takes its dependencies as arguments and returns a
|
|
6
|
+
* {@link ToolOutcome}. Nothing in this file imports the MCP SDK: `server.ts`
|
|
7
|
+
* adapts these to the protocol's content shape. That seam exists so the tests
|
|
8
|
+
* exercise the behaviour rather than the framing, and so the answer to "what
|
|
9
|
+
* does `propose` do when the key is unset" is one function call.
|
|
10
|
+
*
|
|
11
|
+
* # What is deliberately absent
|
|
12
|
+
*
|
|
13
|
+
* There is no approve tool, no simulated approval, and no tool that writes a
|
|
14
|
+
* file. The reason for the first two is worth carrying here rather than leaving
|
|
15
|
+
* in a ticket: **a simulated approval minted on a developer's laptop is a fake
|
|
16
|
+
* receipt factory**, and a receipt factory is the exact artifact this product
|
|
17
|
+
* exists to make impossible.
|
|
18
|
+
*
|
|
19
|
+
* Until ACP-213 this file also said there was no sandbox tool, and that a
|
|
20
|
+
* sandbox mode was "a follow-up with its own design". That follow-up landed and
|
|
21
|
+
* the design is the reason the sentence could change: a Ziffer sandbox is a
|
|
22
|
+
* SEPARATE TENANT with its own receipt signing identity, approved by a robot
|
|
23
|
+
* that walks the real quorum path with keys enrolled only in sandbox bundles
|
|
24
|
+
* (`services/sandbox-approver`, `docs/onboarding/sandbox.md`). Nothing about it
|
|
25
|
+
* is client-side, which is why {@link sandboxStatus} only ever REPORTS: it
|
|
26
|
+
* reads a name and asks the API about one decision. An approval on this side of
|
|
27
|
+
* the wire would still be a forgery, and there is still no tool for it.
|
|
28
|
+
*
|
|
29
|
+
* The third is a boundary rather than a danger: this server has no hands on the
|
|
30
|
+
* developer's machine. It serves knowledge and Ziffer-side calls; the coding
|
|
31
|
+
* agent talking to it is the thing that edits code. A file-writing tool here
|
|
32
|
+
* would make this process an editor with none of an editor's review surface.
|
|
33
|
+
*/
|
|
34
|
+
import { ApiRefusal } from '@ziffer-io/client';
|
|
35
|
+
import { canon, Refusal, verifyReceipt } from '@ziffer-io/verify';
|
|
36
|
+
import { AnchorError, loadTrustAnchor } from './anchor.js';
|
|
37
|
+
import { anchorConfig, ConfigError } from './config.js';
|
|
38
|
+
import { asLanguage, integrationGuide, LANGUAGES } from './guide.js';
|
|
39
|
+
/**
|
|
40
|
+
* Turn any thrown value into an agent-readable refusal.
|
|
41
|
+
*
|
|
42
|
+
* Every branch narrows with `instanceof`; there is no cast. That matters more
|
|
43
|
+
* than usual here because the input is a `catch` binding: the values this sees
|
|
44
|
+
* include the ones nobody planned for, and an `as` would read a `.clause` off a
|
|
45
|
+
* `TypeError` and report a protocol refusal that never happened
|
|
46
|
+
* (`.claude/rules/static-analysis.md`, and section 8 of the guide says the same
|
|
47
|
+
* thing to the reader).
|
|
48
|
+
*/
|
|
49
|
+
function refusal(error) {
|
|
50
|
+
if (error instanceof ConfigError) {
|
|
51
|
+
return { text: `${error.name}: ${error.message}`, isError: true };
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof ApiRefusal) {
|
|
54
|
+
// The gateway's §1 NAME leads, not the class name. `ApiRefusal` carries
|
|
55
|
+
// that name in `.error` and sets `.name` to its own class, so the generic
|
|
56
|
+
// `Error` branch below would report every gateway refusal as "ApiRefusal"
|
|
57
|
+
// and the agent could not tell `TenantMismatch` from `ApiKeyUnknown` —
|
|
58
|
+
// which are different developer actions. The name is the contract; the
|
|
59
|
+
// status is context.
|
|
60
|
+
return { text: `${error.error}: ${error.message}`, isError: true };
|
|
61
|
+
}
|
|
62
|
+
if (error instanceof AnchorError) {
|
|
63
|
+
return { text: error.message, isError: true };
|
|
64
|
+
}
|
|
65
|
+
if (error instanceof Refusal) {
|
|
66
|
+
// The clause is the machine-readable half and is named first, because it is
|
|
67
|
+
// the value an agent should quote back to a human and the one that is
|
|
68
|
+
// spelled the same in all three implementations.
|
|
69
|
+
return { text: `refused: ${error.clause}\n${error.message}`, isError: true };
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
return { text: `${error.name}: ${error.message}`, isError: true };
|
|
73
|
+
}
|
|
74
|
+
return { text: `UnknownError: ${String(error)}`, isError: true };
|
|
75
|
+
}
|
|
76
|
+
/** JSON as an agent should read it: stable, indented, and never a bare value. */
|
|
77
|
+
function asJson(value) {
|
|
78
|
+
return JSON.stringify(value, null, 2);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `propose` — submit one wire Proposal and return the §1 response verbatim.
|
|
82
|
+
*
|
|
83
|
+
* The proposal is passed through untouched. This server does not fill in a
|
|
84
|
+
* `tenant_id`, and it must not: ACP-197 section 1 makes the KEY the tenant, and
|
|
85
|
+
* a body whose `tenant_id` disagrees is refused rather than rewritten, because
|
|
86
|
+
* the proposal is signed material downstream and a rewriter would have become
|
|
87
|
+
* its author. Helpfully "correcting" the field here would produce exactly the
|
|
88
|
+
* silent authorship the gateway's refusal exists to prevent.
|
|
89
|
+
*/
|
|
90
|
+
export async function propose(clientFor, env, proposal) {
|
|
91
|
+
try {
|
|
92
|
+
const client = await clientFor(env);
|
|
93
|
+
return { text: asJson(await client.propose(proposal)), isError: false };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return refusal(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `check_decision` — one decision by id, receipt included when present.
|
|
101
|
+
*
|
|
102
|
+
* The receipt is re-serialised as part of the response object rather than
|
|
103
|
+
* spliced in as text. That is safe HERE, and only here, because this output is
|
|
104
|
+
* for an agent to read: the copy that gets VERIFIED is the one
|
|
105
|
+
* `explain_receipt` is handed, and the hash it checks is recomputed from the
|
|
106
|
+
* proposal bytes, never from a re-encoding of the receipt. Nothing downstream
|
|
107
|
+
* of this tool treats its text as signed material.
|
|
108
|
+
*/
|
|
109
|
+
export async function checkDecision(clientFor, env, decisionId) {
|
|
110
|
+
try {
|
|
111
|
+
const client = await clientFor(env);
|
|
112
|
+
return { text: asJson(await client.decision(decisionId)), isError: false };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return refusal(error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `get_integration_guide` — `docs/onboarding/sdk.md`, for one language.
|
|
120
|
+
*
|
|
121
|
+
* The one tool that needs no configuration, deliberately: an agent should be
|
|
122
|
+
* able to read how to integrate before any credential exists. That is also why
|
|
123
|
+
* `config.ts` lets the server start unconfigured — a server that refused to
|
|
124
|
+
* boot without an API key could not answer the question a developer asks first.
|
|
125
|
+
*/
|
|
126
|
+
export function getIntegrationGuide(language) {
|
|
127
|
+
const known = asLanguage(language);
|
|
128
|
+
if (known === null) {
|
|
129
|
+
return {
|
|
130
|
+
text: `UnknownLanguage: ${JSON.stringify(language)} is not one of ${LANGUAGES.join(', ')}.`,
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return { text: integrationGuide(known), isError: false };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return refusal(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The marker's naming half (ACP-213). One spelling, and it is not the one that
|
|
143
|
+
* decides: `services/sandbox-approver/src/sandbox.rs` holds the rule that
|
|
144
|
+
* governs, against the tenant the SIGNED bundle names, beside an allowlist of
|
|
145
|
+
* receipt identities no client can see. This copy exists so a developer is told
|
|
146
|
+
* what they are pointed at before they send a proposal, and it says so.
|
|
147
|
+
*/
|
|
148
|
+
export const SANDBOX_SUFFIX = '-sandbox';
|
|
149
|
+
/**
|
|
150
|
+
* Is this tenant id spelled as a sandbox?
|
|
151
|
+
*
|
|
152
|
+
* The suffix has to be attached to something: a tenant literally named
|
|
153
|
+
* `-sandbox` would be a tenant with an empty production name, which satisfies
|
|
154
|
+
* the convention while meaning nothing. Same rule, same words, as the Rust
|
|
155
|
+
* predicate and `tools/provision-sandbox.py`.
|
|
156
|
+
*/
|
|
157
|
+
export function isSandboxName(tenantId) {
|
|
158
|
+
return tenantId.length > SANDBOX_SUFFIX.length && tenantId.endsWith(SANDBOX_SUFFIX);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `sandbox_status` — say whether this tenant is a sandbox, and whether the
|
|
162
|
+
* robot approver has acted on one decision.
|
|
163
|
+
*
|
|
164
|
+
* # What it can establish, and what it cannot
|
|
165
|
+
*
|
|
166
|
+
* It reports the NAMING half of the marker over the tenant id the caller will
|
|
167
|
+
* actually send, and it names the half it cannot see. The binding half — that
|
|
168
|
+
* this tenant's receipt signing identity is registered in the robot's
|
|
169
|
+
* allowlist — is checked inside the deployment, against signed policy, on every
|
|
170
|
+
* `/v1/present`. There is no endpoint that exposes it and this tool does not
|
|
171
|
+
* invent one: a client-side "you are in a sandbox" that rested on nothing would
|
|
172
|
+
* be worse than no answer, because a developer would plan around it.
|
|
173
|
+
*
|
|
174
|
+
* Liveness is the same discipline. There is no health route on the gateway, so
|
|
175
|
+
* the honest signal is one decision's own status. **The signature of a stopped
|
|
176
|
+
* robot is `decided` with `outcome: "ATTEST"` and no receipt** — not `pending`,
|
|
177
|
+
* which is what this said until the 2026-09-03 live run of
|
|
178
|
+
* `tools/rehearse/03-loop.sh` showed a floor-HIGH sandbox proposal answering
|
|
179
|
+
* `decided`/`ATTEST` in the POST itself and never passing through `pending` at
|
|
180
|
+
* all. So the advice that could never fire moved to the branch that sees the
|
|
181
|
+
* real thing. A quorum-awaiting decision keeps that outcome and grows a
|
|
182
|
+
* RECEIPT; a decision that never grows one is the robot being stopped or
|
|
183
|
+
* mis-enrolled, and the text says which of the cases it saw rather than
|
|
184
|
+
* reducing them to a green tick.
|
|
185
|
+
*
|
|
186
|
+
* # Why the tenant is an argument and not configuration
|
|
187
|
+
*
|
|
188
|
+
* Because this server has never held a tenant name and must not start: ACP-197
|
|
189
|
+
* section 1 makes the KEY the tenant, and a `ZIFFER_TENANT` variable would be a
|
|
190
|
+
* second statement of it that could disagree with the key. The tenant id is a
|
|
191
|
+
* value the caller already writes into every proposal — where the gateway binds
|
|
192
|
+
* it, refusing `TenantMismatch` rather than rewriting it — so checking the
|
|
193
|
+
* string the caller is about to send is checking the thing that matters.
|
|
194
|
+
*/
|
|
195
|
+
export async function sandboxStatus(clientFor, env, args) {
|
|
196
|
+
const tenant = args.tenantId.trim();
|
|
197
|
+
if (tenant === '') {
|
|
198
|
+
return {
|
|
199
|
+
text: 'TenantUnnamed: pass the tenant_id your proposals carry; this server holds no tenant of its own.',
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const lines = [`tenant: ${JSON.stringify(tenant)}`];
|
|
204
|
+
if (isSandboxName(tenant)) {
|
|
205
|
+
lines.push(`sandbox: yes, by name — it ends in "${SANDBOX_SUFFIX}".`, ' What that guarantees: a sandbox tenant is a SEPARATE tenant whose bundle names its own', ' receipt signing identity, so receipts issued here are signed by a different key and a', ' production verifier refuses them (clause 9.3-1). You cannot get a production-valid', ' receipt out of a sandbox, by construction rather than by policy.', ' What it does NOT mean: the action is not performed for real by anything Ziffer runs, and', ' approvals here are made by a robot approver with no human in them. Treat every ALLOW as', ' "the path worked", never as "someone agreed".', ' What this tool cannot see: the other half of the marker is an allowlist of receipt', ' identities the robot approver reads out of signed policy inside the deployment. It is', ' checked there, on every approval, and no client can observe it.');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
lines.push(`sandbox: no, by name — it does not end in "${SANDBOX_SUFFIX}".`, ' Receipts for this tenant are production receipts and its approvals are made by whoever', ' its bundle enrols. Nothing here is auto-approved. If you meant to be in a sandbox, you', ` want the separate tenant "${tenant}${SANDBOX_SUFFIX}" and its own API key — the key`, ' determines the tenant, so pointing at a sandbox is a credential change, not a flag.');
|
|
209
|
+
}
|
|
210
|
+
const decisionId = args.decisionId?.trim();
|
|
211
|
+
if (decisionId === undefined || decisionId === '') {
|
|
212
|
+
lines.push('approver: not checked — pass decision_id to have this tool ask the API about one decision.');
|
|
213
|
+
return { text: lines.join('\n'), isError: false };
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const client = await clientFor(env);
|
|
217
|
+
const record = await client.decision(decisionId);
|
|
218
|
+
const outcome = record.outcome === undefined ? '' : ` outcome ${record.outcome}`;
|
|
219
|
+
const clause = record.clause === undefined ? '' : ` clause ${record.clause}`;
|
|
220
|
+
if (record.status === 'decided') {
|
|
221
|
+
lines.push(`approver: decision ${record.decision_id} is decided —${outcome}${clause}`.trimEnd());
|
|
222
|
+
if (record.receipt === undefined) {
|
|
223
|
+
lines.push(' No signed receipt is attached, so nothing here is verifiable: read the clause. A', ' decided path is evidence the deployment answered, not that a quorum formed.');
|
|
224
|
+
if (record.outcome === 'ATTEST') {
|
|
225
|
+
lines.push(' ATTEST is the §8.6 quorum gate and not a refusal — this decision is waiting on', ' approvers, and in a sandbox that is the robot. The receipt attaches to THIS same', ' decision on a later check_decision while the outcome stays ATTEST, so poll for the', ' RECEIPT and never for a change of outcome. Do not re-propose: a second proposal is', ' a second action. A decided ATTEST that never grows a receipt is what a stopped or', ' mis-enrolled robot approver looks like from out here. There is no health endpoint;', ' this is the only liveness signal the API exposes.');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
lines.push(' A signed receipt is attached. Verify it with explain_receipt: this tool checked no', ' signature, and a status field is not a signature.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines.push(`approver: decision ${record.decision_id} is still pending.`, ' Pending means the deployment has not answered this decision yet. It is NOT the', ' signature of a stopped approver: a decision waiting on its quorum reads decided with', ' outcome ATTEST and no receipt, never pending. Ask again in a moment.');
|
|
234
|
+
}
|
|
235
|
+
return { text: lines.join('\n'), isError: false };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// The naming half already held, and losing it would make a gateway blip
|
|
239
|
+
// read as "your tenant is not a sandbox". The refusal is appended, with its
|
|
240
|
+
// own name intact, rather than replacing what was established.
|
|
241
|
+
const failed = refusal(error);
|
|
242
|
+
return { text: `${lines.join('\n')}\napprover: not established — ${failed.text}`, isError: true };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `explain_receipt` — verify a receipt and say `valid` or the named clause.
|
|
247
|
+
*
|
|
248
|
+
* **Zero verification logic of its own.** Every check is `@ziffer-io/verify`'s; this
|
|
249
|
+
* function reads a key file, decodes a base64 argument and formats an answer.
|
|
250
|
+
* A second verifier living in a developer-tools package would be a fourth
|
|
251
|
+
* implementation of §9.3 whose disagreements with the other three would be
|
|
252
|
+
* discovered by a customer.
|
|
253
|
+
*
|
|
254
|
+
* # Why the proposal arrives as bytes and not as an object
|
|
255
|
+
*
|
|
256
|
+
* Because `verifyReceipt` takes bytes, and this tool exists to reproduce the
|
|
257
|
+
* call the developer's own code makes. Accepting an object and encoding it here
|
|
258
|
+
* would put an encoding step between their input and the verifier that their
|
|
259
|
+
* production path does not have, and it would hide one real failure class
|
|
260
|
+
* outright: bytes that are not UTF-8 JSON at all — a truncated read, a
|
|
261
|
+
* double-encoded string, a compressed body — which the verifier refuses under
|
|
262
|
+
* `AT-8a`. Handed an object, this tool could never see that.
|
|
263
|
+
*
|
|
264
|
+
* What it is NOT for is key order. An earlier draft of this file, and of the
|
|
265
|
+
* guide, claimed the caller had to pass `canon(proposal)` and that
|
|
266
|
+
* `JSON.stringify` would produce a spurious `9.3-3`. That is false, and the
|
|
267
|
+
* check that showed it was running all three encodings through this function:
|
|
268
|
+
* `verifyReceipt` PARSES these bytes and canonicalises them itself, because the
|
|
269
|
+
* hash is defined over the canonical encoding rather than over the transport's
|
|
270
|
+
* spacing (`verify.ts` step 3; the Python SDK's `verify` does the same). Any
|
|
271
|
+
* JSON spelling of one object verifies. The hint below therefore names the
|
|
272
|
+
* cause that is real — a different object — rather than the one that reads
|
|
273
|
+
* plausibly.
|
|
274
|
+
*/
|
|
275
|
+
export async function explainReceipt(env, args) {
|
|
276
|
+
try {
|
|
277
|
+
const config = anchorConfig(env, args.trustAnchorPath);
|
|
278
|
+
const anchor = await loadTrustAnchor(config.anchorPath, config.suiteFloor);
|
|
279
|
+
let proposalBytes;
|
|
280
|
+
try {
|
|
281
|
+
proposalBytes = Uint8Array.from(Buffer.from(args.proposalB64, 'base64'));
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return {
|
|
285
|
+
text: 'ProposalBytesMalformed: proposal_b64 is not base64.',
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Node's base64 decoder is lenient: it drops characters it does not
|
|
290
|
+
// recognise rather than throwing, so a caller who passed hex, or a JSON
|
|
291
|
+
// string, gets silent garbage and a 9.3-3 refusal blaming the receipt.
|
|
292
|
+
// Re-encoding and comparing is the cheap way to catch that, and the refusal
|
|
293
|
+
// names the argument rather than the receipt.
|
|
294
|
+
if (Buffer.from(proposalBytes).toString('base64') !== args.proposalB64.trim()) {
|
|
295
|
+
return {
|
|
296
|
+
text: 'ProposalBytesMalformed: proposal_b64 did not survive a base64 round trip, so it is not the encoding it claims.\n' +
|
|
297
|
+
' Pass the bytes your code passes to verify, base64-encoded.',
|
|
298
|
+
isError: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const verified = verifyReceipt(args.receipt, proposalBytes, anchor);
|
|
302
|
+
return {
|
|
303
|
+
text: `valid: bound to ${verified.proposalHash}\n` +
|
|
304
|
+
` suite floor ${config.suiteFloor}, anchor ${config.anchorPath}\n` +
|
|
305
|
+
` receipt expires at ${new Date(verified.receiptExpiresAt * 1000).toISOString()}\n` +
|
|
306
|
+
' This receipt verified. It says nothing about whether it was already used:\n' +
|
|
307
|
+
' replay is a claim against a ledger this process cannot reach (guide section 6, item 2).',
|
|
308
|
+
isError: false,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
const outcome = refusal(error);
|
|
313
|
+
if (error instanceof Refusal && error.clause === '9.3-3') {
|
|
314
|
+
// The one refusal worth a hint, because the obvious suspect is the wrong
|
|
315
|
+
// one. Appended to the named clause, never substituted for it.
|
|
316
|
+
return {
|
|
317
|
+
text: `${outcome.text}\n` +
|
|
318
|
+
' The receipt is bound to a DIFFERENT proposal. It is not key order or whitespace:\n' +
|
|
319
|
+
' the verifier parses these bytes and canonicalises them itself, so any JSON spelling\n' +
|
|
320
|
+
' of one object hashes the same. Look for a changed field value, a field added or\n' +
|
|
321
|
+
' dropped after the proposal was submitted, or the receipt of another decision.',
|
|
322
|
+
isError: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return outcome;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Re-exported so `server.ts` and the tests name one canonicalisation. */
|
|
329
|
+
export { canon };
|
|
330
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziffer-io/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local MCP server over stdio so a coding agent can integrate the ZIFFER SDK and drive a live decision loop.",
|
|
5
|
+
"author": "code75 SASU",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"comment-license": "The SDK is proprietary (code75 SASU). \"license\" is the SPDX escape hatch for exactly this case: there is no SPDX identifier for these terms, so the field points at the file that states them, and LICENSE ships in the tarball beside THIRD-PARTY-NOTICES. Do not put an OSI identifier here -- Apache-2.0 stood in these four files until ACP-214 and was wrong the whole time.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"ziffer-mcp": "./dist/bin.js"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/**/*.test.*",
|
|
24
|
+
"THIRD-PARTY-NOTICES"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/ziffer-hq/ziffer.git",
|
|
32
|
+
"directory": "packages/mcp"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://ziffer.io",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"comment-no-provenance": "There is deliberately no \"provenance\": true here, and it was removed rather than never added. npm generates a provenance attestation only from a recognised CI runner, and its documentation states plainly that provenance is NOT SUPPORTED for private repositories (docs.npmjs.com/trusted-publishers, read 2026-09-03) -- ziffer-hq/ziffer is private (`gh api` says so). Setting the flag does not degrade to a warning: npm attempts the attestation and the publish FAILS, so the field would have broken the operator's very first publish from a laptop and every CI publish after it. tools/release-npm.sh asserts the field stays absent, and that assertion is the thing to delete on the day this repository becomes public -- at which point trusted publishing generates provenance on its own, with no flag at all.",
|
|
39
|
+
"ziffer": {
|
|
40
|
+
"enginePin": "fed43d10b427e0a4435a8f04e474334d88a5aa6e"
|
|
41
|
+
},
|
|
42
|
+
"comment-enginePin": "The engine commit this server's verifier and wire types come from, through @ziffer-io/verify and @ziffer-io/client. A COPY of the rev in Cargo.toml, which is the one authority tools/guard.sh reads; tools/release-npm.sh refuses to release when the two differ, by name. tools/bump-pin.sh does not move this field -- see packages/types/package.json for why and what closing it costs.",
|
|
43
|
+
"comment-private-removed": "`private: true` is gone (ACP-214). It was here because the whole point is `npx @ziffer-io/mcp` and this package depended on two workspace packages that had no public home, so publishing it would have handed a developer a command that installs and then cannot start. All four now publish together, in dependency order, from tools/release-npm.sh -- @ziffer-io/types, then verify, then client, then this. NOTE WHAT REMOVING THE FLAG DOES AND DOES NOT DO: it makes publication POSSIBLE, not done. Nothing is on the registry until the operator runs the publish lines the release script prints and never executes, and packages/mcp/README.md's 'What does not work yet' says so in those words rather than leaving a reader to infer it from this file.",
|
|
44
|
+
"comment-deps": "@modelcontextprotocol/sdk is a new dependency and it IS the protocol -- an MCP server that hand-rolls JSON-RPC framing over stdio is a second implementation of a wire format someone else owns (ACP-197 runbook section 6b point 1 justifies it by name). zod is NOT a fourth choice: it is that SDK's declared peer dependency and the type its registerTool input schemas are written in, so it arrives with the SDK or the SDK does not work. It is confined to the tool-schema layer, which is this package's contracts layer, exactly as .claude/rules/typescript.md requires. @ziffer-io/verify is the ONE home of receipt verification (runbook section 6b point 2: zero verification logic of our own) and @ziffer-io/client the one home of the HTTP surface.",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
47
|
+
"zod": "4.5.4",
|
|
48
|
+
"@ziffer-io/client": "0.1.0",
|
|
49
|
+
"@ziffer-io/verify": "0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"comment-devdeps": "@noble/curves and @noble/post-quantum are TEST-ONLY here and ship in nothing: explain_receipt's positive path and its 9.3-3 hint both sit BEHIND signature verification, so a test that cannot sign can only ever assert the refusal it stops at first, and those two branches would go to a customer unexercised. They are the versions services/approval and packages/acp-verify already pin, so this adds no new code to the tree -- and the alternative considered and rejected was re-deriving test keys locally, which would be two definitions of one identity. `files: [dist]` is what keeps them out of the tarball; they still appear in the PUBLISHED package.json, because pnpm pack does not strip devDependencies (measured), and that is cosmetic rather than a resolution a consumer performs.",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@noble/curves": "2.3.0",
|
|
54
|
+
"@noble/post-quantum": "0.7.0",
|
|
55
|
+
"@types/node": "^22.15.0",
|
|
56
|
+
"typescript": "^5.9.2"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"embed": "node scripts/embed-guide.mjs",
|
|
60
|
+
"build": "pnpm run embed && tsc -b",
|
|
61
|
+
"typecheck": "pnpm run embed && tsc -b",
|
|
62
|
+
"test": "pnpm run embed && tsc -b && node scripts/check-tests-built.mjs && node --test \"dist/**/*.test.js\""
|
|
63
|
+
}
|
|
64
|
+
}
|