@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.
@@ -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
+ }