@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/anchor.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a {@link TrustAnchor} off disk, from the document the engine's own tool
|
|
3
|
+
* writes (ACP-197, section 6b point 3).
|
|
4
|
+
*
|
|
5
|
+
* # The format is not ours to choose
|
|
6
|
+
*
|
|
7
|
+
* `ZIFFER_TRUST_ANCHOR` names the output of `acp-bundle pubkey --key <file>`,
|
|
8
|
+
* at engine pin fed43d1, `crates/acp-bundle-cli/src/main.rs::cmd_pubkey` (the
|
|
9
|
+
* same four keys in the same order it wrote at 356ef8e, where this comment was
|
|
10
|
+
* first written; re-read at the v1.3.32 pin rather than assumed). That
|
|
11
|
+
* document exists precisely so an operator stops transcribing a key between
|
|
12
|
+
* three encodings by hand, and its doc comment says why a slip matters: **a
|
|
13
|
+
* transcription slip does not surface as a transcription slip** -- it surfaces
|
|
14
|
+
* as a signature that does not verify, wearing the face of a compromise. A
|
|
15
|
+
* second file format here would put that slip back, one layer up.
|
|
16
|
+
*
|
|
17
|
+
* The fields read are the two the engine's own `load_public_key` reads, because
|
|
18
|
+
* the top level of that document IS the `--pubkey` file:
|
|
19
|
+
*
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "ed25519_pk_hex": "<64 hex chars>",
|
|
23
|
+
* "fingerprint": "sha256:<hex>",
|
|
24
|
+
* "identity": { "classical": "<base64>", "pq": "<base64>" },
|
|
25
|
+
* "mldsa65_pk_hex": "<3904 hex chars>"
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `identity` and `fingerprint` are other encodings of the same two keys and are
|
|
30
|
+
* deliberately NOT read: reading a key twice from one file is two answers to
|
|
31
|
+
* "which identity is this", and the one nobody exercises is the wrong one.
|
|
32
|
+
*
|
|
33
|
+
* # Why the suite floor is not in this file
|
|
34
|
+
*
|
|
35
|
+
* {@link TrustAnchor} needs a third value, `minSuite`, and this document does
|
|
36
|
+
* not carry it -- by construction. `cmd_pubkey` refuses to emit an `alg` field
|
|
37
|
+
* and states the rule: a suite name is a WIRE field (CR-1), never a member of
|
|
38
|
+
* an identity, and putting one beside a key would invent a shape no schema in
|
|
39
|
+
* the engine declares. So the floor is separate configuration, in
|
|
40
|
+
* `ZIFFER_SUITE_FLOOR`, with no default -- which is also the shape the Executor
|
|
41
|
+
* already ships (`ZIFFER_POLICY_SIGNING_KEY` names the key file and
|
|
42
|
+
* `ZIFFER_EXECUTOR_SUITE_FLOOR` names the floor beside it,
|
|
43
|
+
* `docs/onboarding/executor.md` section 4: "an unknown suite is refused, never
|
|
44
|
+
* defaulted").
|
|
45
|
+
*/
|
|
46
|
+
import { readFile } from 'node:fs/promises';
|
|
47
|
+
/** Ed25519 verification key: 32 bytes, so 64 hex characters. */
|
|
48
|
+
const ED25519_PK_HEX_LEN = 64;
|
|
49
|
+
/** ML-DSA-65 verification key: 1,952 bytes, so 3,904 hex characters. */
|
|
50
|
+
const MLDSA65_PK_HEX_LEN = 3904;
|
|
51
|
+
/**
|
|
52
|
+
* The secret-half field names `acp-bundle` writes into a KEY file.
|
|
53
|
+
*
|
|
54
|
+
* Checked for by name because the mistake this catches is a plausible one: the
|
|
55
|
+
* operator has two JSON files with similar names, and points
|
|
56
|
+
* `ZIFFER_TRUST_ANCHOR` at the signing key instead of the public document. That
|
|
57
|
+
* file has no `ed25519_pk_hex`, so without this check the refusal would be
|
|
58
|
+
* `TrustAnchorMalformed: missing ed25519_pk_hex` -- true, unhelpful, and it
|
|
59
|
+
* would send the developer to inspect a file holding a live private key while
|
|
60
|
+
* wondering what field to add to it. `cmd_pubkey` guards the same confusion
|
|
61
|
+
* from the other direction with `refuse_if_secret_present`.
|
|
62
|
+
*/
|
|
63
|
+
const SECRET_FIELDS = ['ed25519_sk_hex', 'mldsa65_sk_hex'];
|
|
64
|
+
/**
|
|
65
|
+
* A trust anchor file that is missing, unreadable, or not what it claims to be.
|
|
66
|
+
*
|
|
67
|
+
* Named like the configuration refusals in `config.ts` and for the same reason:
|
|
68
|
+
* the agent reading this needs to act, and "the file you named is a signing
|
|
69
|
+
* key" and "the file you named does not exist" are different actions.
|
|
70
|
+
*
|
|
71
|
+
* `detail` never carries file CONTENT. A malformed anchor is quoted back into
|
|
72
|
+
* an agent's context, and if the file turned out to be a secret key, echoing
|
|
73
|
+
* the bytes that made it malformed is how the key gets there.
|
|
74
|
+
*/
|
|
75
|
+
export class AnchorError extends Error {
|
|
76
|
+
name;
|
|
77
|
+
/** The path that was read, which is safe to name and is what identifies it. */
|
|
78
|
+
path;
|
|
79
|
+
constructor(name, path, detail) {
|
|
80
|
+
super(`${name}: ${path}: ${detail}`);
|
|
81
|
+
this.name = name;
|
|
82
|
+
this.path = path;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isRecord(v) {
|
|
86
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decode a lowercase-hex field of an exact length.
|
|
90
|
+
*
|
|
91
|
+
* The length is checked before the decode and is exact rather than a minimum,
|
|
92
|
+
* because a key of the wrong length is not a weak key, it is a different
|
|
93
|
+
* object: 31 bytes of an Ed25519 key is not an Ed25519 key at all, and
|
|
94
|
+
* `verifyReceipt` would refuse every signature under it with a clause that
|
|
95
|
+
* blames the receipt for a defect in the anchor.
|
|
96
|
+
*/
|
|
97
|
+
function hexField(doc, key, expected, path) {
|
|
98
|
+
const raw = doc[key];
|
|
99
|
+
if (typeof raw !== 'string') {
|
|
100
|
+
throw new AnchorError('TrustAnchorMalformed', path, `${key} is ${raw === undefined ? 'absent' : 'not a string'}; this is not an \`acp-bundle pubkey\` document.`);
|
|
101
|
+
}
|
|
102
|
+
if (raw.length !== expected) {
|
|
103
|
+
throw new AnchorError('TrustAnchorMalformed', path, `${key} is ${raw.length} hex characters, expected exactly ${expected}.`);
|
|
104
|
+
}
|
|
105
|
+
if (!/^[0-9a-f]+$/.test(raw)) {
|
|
106
|
+
// Lowercase only, matching what `cmd_pubkey` writes. Accepting uppercase
|
|
107
|
+
// would mean two spellings of one anchor file compare unequal while
|
|
108
|
+
// decoding to the same key, and an operator diffing a rotated key against
|
|
109
|
+
// the previous one could not tell which changed.
|
|
110
|
+
throw new AnchorError('TrustAnchorMalformed', path, `${key} is not lowercase hexadecimal.`);
|
|
111
|
+
}
|
|
112
|
+
const out = new Uint8Array(raw.length / 2);
|
|
113
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
114
|
+
out[i] = Number.parseInt(raw.slice(i * 2, i * 2 + 2), 16);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read and parse the anchor document at `path`, or refuse by name.
|
|
120
|
+
*
|
|
121
|
+
* @param minSuite the CR-4 floor from `ZIFFER_SUITE_FLOOR`, passed through
|
|
122
|
+
* unvalidated: `verifyReceipt` is the one place that knows
|
|
123
|
+
* which suite names exist, and it refuses an unknown floor
|
|
124
|
+
* under `CLAUSE_UNKNOWN_SUITE`. A second table of suite names
|
|
125
|
+
* here would be a second answer to what a suite is.
|
|
126
|
+
* @throws AnchorError `TrustAnchorUnreadable`, `TrustAnchorNotJson`,
|
|
127
|
+
* `TrustAnchorHoldsSecret` or `TrustAnchorMalformed`.
|
|
128
|
+
*/
|
|
129
|
+
export async function loadTrustAnchor(path, minSuite) {
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = await readFile(path, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
// The cause's message is included because "no such file" and "permission
|
|
136
|
+
// denied" are different developer actions and both are safe to say: it is
|
|
137
|
+
// errno text about a path, not content from inside the file.
|
|
138
|
+
throw new AnchorError('TrustAnchorUnreadable', path, error instanceof Error ? error.message : 'could not be read.');
|
|
139
|
+
}
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new AnchorError('TrustAnchorNotJson', path, 'the file is not JSON; expected the output of `acp-bundle pubkey --key <file>`.');
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(parsed)) {
|
|
148
|
+
throw new AnchorError('TrustAnchorNotJson', path, 'the file is JSON but not an object.');
|
|
149
|
+
}
|
|
150
|
+
for (const secret of SECRET_FIELDS) {
|
|
151
|
+
if (secret in parsed) {
|
|
152
|
+
throw new AnchorError('TrustAnchorHoldsSecret', path, `this file carries ${secret}, so it is a SIGNING KEY, not a public trust anchor. Nothing was read from it. Run \`acp-bundle pubkey --key <this file> --out <anchor file>\` and point ZIFFER_TRUST_ANCHOR at the output.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
classical: hexField(parsed, 'ed25519_pk_hex', ED25519_PK_HEX_LEN, path),
|
|
157
|
+
pq: hexField(parsed, 'mldsa65_pk_hex', MLDSA65_PK_HEX_LEN, path),
|
|
158
|
+
minSuite,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=anchor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor.js","sourceRoot":"","sources":["../src/anchor.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx @ziffer-io/mcp` — the entry point (ACP-197, section 6b).
|
|
4
|
+
*
|
|
5
|
+
* # Everything human goes to stderr
|
|
6
|
+
*
|
|
7
|
+
* stdout is the JSON-RPC stream. A single stray `console.log` — a banner, a
|
|
8
|
+
* version line, a debug print — is not a cosmetic problem: it lands in the
|
|
9
|
+
* middle of a framed message and the client's parser fails on a protocol error
|
|
10
|
+
* that names nothing about where it came from. So this file writes to stderr,
|
|
11
|
+
* which MCP clients surface as server logs, and nothing else in this package
|
|
12
|
+
* writes to either.
|
|
13
|
+
*
|
|
14
|
+
* # It does not check its configuration before starting
|
|
15
|
+
*
|
|
16
|
+
* Deliberately, and `config.ts` carries the argument: a stdio server that exits
|
|
17
|
+
* during the handshake tells the agent only that the server failed to start,
|
|
18
|
+
* and the variable that was missing dies on a stderr nobody reads. Starting and
|
|
19
|
+
* refusing per tool puts the variable name in front of the one party that can
|
|
20
|
+
* fix it. The startup line below reports what is configured so a developer who
|
|
21
|
+
* IS reading the log gets the same information early — a report, not a gate.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=bin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;GAoBG"}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx @ziffer-io/mcp` — the entry point (ACP-197, section 6b).
|
|
4
|
+
*
|
|
5
|
+
* # Everything human goes to stderr
|
|
6
|
+
*
|
|
7
|
+
* stdout is the JSON-RPC stream. A single stray `console.log` — a banner, a
|
|
8
|
+
* version line, a debug print — is not a cosmetic problem: it lands in the
|
|
9
|
+
* middle of a framed message and the client's parser fails on a protocol error
|
|
10
|
+
* that names nothing about where it came from. So this file writes to stderr,
|
|
11
|
+
* which MCP clients surface as server logs, and nothing else in this package
|
|
12
|
+
* writes to either.
|
|
13
|
+
*
|
|
14
|
+
* # It does not check its configuration before starting
|
|
15
|
+
*
|
|
16
|
+
* Deliberately, and `config.ts` carries the argument: a stdio server that exits
|
|
17
|
+
* during the handshake tells the agent only that the server failed to start,
|
|
18
|
+
* and the variable that was missing dies on a stderr nobody reads. Starting and
|
|
19
|
+
* refusing per tool puts the variable name in front of the one party that can
|
|
20
|
+
* fix it. The startup line below reports what is configured so a developer who
|
|
21
|
+
* IS reading the log gets the same information early — a report, not a gate.
|
|
22
|
+
*/
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { VARS } from './config.js';
|
|
25
|
+
import { createDefaultServer } from './server.js';
|
|
26
|
+
/**
|
|
27
|
+
* Which variables are set, never their values.
|
|
28
|
+
*
|
|
29
|
+
* `ZIFFER_API_KEY` is a bearer credential and this line goes to a log an agent
|
|
30
|
+
* may capture into its own context. Presence is the whole of what is useful
|
|
31
|
+
* here anyway: the question this answers is "did my environment reach the
|
|
32
|
+
* server", and a boolean answers it.
|
|
33
|
+
*/
|
|
34
|
+
function configurationSummary() {
|
|
35
|
+
return Object.values(VARS)
|
|
36
|
+
.map((variable) => {
|
|
37
|
+
const raw = process.env[variable];
|
|
38
|
+
return `${variable}=${raw === undefined || raw.trim() === '' ? 'unset' : 'set'}`;
|
|
39
|
+
})
|
|
40
|
+
.join(' ');
|
|
41
|
+
}
|
|
42
|
+
async function main() {
|
|
43
|
+
const server = createDefaultServer();
|
|
44
|
+
await server.connect(new StdioServerTransport());
|
|
45
|
+
process.stderr.write(`ziffer mcp: serving on stdio (${configurationSummary()})\n`);
|
|
46
|
+
}
|
|
47
|
+
main().catch((error) => {
|
|
48
|
+
// Reached only if the transport itself fails — a tool refusal never lands
|
|
49
|
+
// here, because every handler returns its refusal as a result. Exiting
|
|
50
|
+
// non-zero matters: an MCP client restarts or reports a server that died, and
|
|
51
|
+
// a process that lingers after its transport is gone is one the client waits
|
|
52
|
+
// on for ever.
|
|
53
|
+
process.stderr.write(`ziffer mcp: could not serve: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;GAOG;AACH,SAAS,oBAAoB;IAC3B,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;SACvB,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClC,OAAO,GAAG,QAAQ,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IACnF,CAAC,CAAC;SACD,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;IACrC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iCAAiC,oBAAoB,EAAE,KAAK,CAAC,CAAC;AACrF,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,0EAA0E;IAC1E,uEAAuE;IACvE,8EAA8E;IAC9E,6EAA6E;IAC7E,eAAe;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gCAAgC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC3F,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one file that knows the concrete `@ziffer-io/client` (ACP-197, section 6b
|
|
3
|
+
* point 1).
|
|
4
|
+
*
|
|
5
|
+
* `tools.ts` calls a {@link DecisionClient}, an interface. This module is where
|
|
6
|
+
* that interface is satisfied by the real thing, and it is deliberately the
|
|
7
|
+
* only import site: if §6's spelling of a field ever differs from §1's wire
|
|
8
|
+
* spelling, the mapping belongs here and nowhere else. Two files translating
|
|
9
|
+
* between the same two vocabularies is how they come to disagree.
|
|
10
|
+
*
|
|
11
|
+
* # What is deliberately NOT here
|
|
12
|
+
*
|
|
13
|
+
* An HTTP client. This package will not carry one even when `@ziffer-io/client` is
|
|
14
|
+
* unavailable: a second implementation of §1 is a second place that knows the
|
|
15
|
+
* auth header, the paths, the status codes and the receipt-passthrough rule,
|
|
16
|
+
* and the second one is always the one that drifts. `docs/onboarding/sdk.md`
|
|
17
|
+
* tells developers there is one client; this package would be the
|
|
18
|
+
* counterexample.
|
|
19
|
+
*
|
|
20
|
+
* There is also no retry, no backoff and no caching. `wait` is the client's own
|
|
21
|
+
* polling loop and it is the one place that decides how often to ask; a second
|
|
22
|
+
* loop here would be two answers to "how hard do we poll", and the tool would
|
|
23
|
+
* be holding a decision the developer's own code has not seen.
|
|
24
|
+
*/
|
|
25
|
+
import type { ClientFactory } from './tools.js';
|
|
26
|
+
/**
|
|
27
|
+
* Build a client from the environment, or refuse.
|
|
28
|
+
*
|
|
29
|
+
* Configuration is resolved first and the refusal it raises reaches the agent
|
|
30
|
+
* as the tool's result, so an unset key is reported before any socket is
|
|
31
|
+
* opened. {@link ZifferClient} satisfies {@link DecisionClient} structurally —
|
|
32
|
+
* §6 fixed that surface and `tools.ts` names the two calls used — so there is
|
|
33
|
+
* no adapter here and no cast: had the two disagreed, this is the one file
|
|
34
|
+
* where the mapping would live.
|
|
35
|
+
*/
|
|
36
|
+
export declare const zifferClientFactory: ClientFactory;
|
|
37
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,YAAY,CAAC;AAEhE;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAGjC,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one file that knows the concrete `@ziffer-io/client` (ACP-197, section 6b
|
|
3
|
+
* point 1).
|
|
4
|
+
*
|
|
5
|
+
* `tools.ts` calls a {@link DecisionClient}, an interface. This module is where
|
|
6
|
+
* that interface is satisfied by the real thing, and it is deliberately the
|
|
7
|
+
* only import site: if §6's spelling of a field ever differs from §1's wire
|
|
8
|
+
* spelling, the mapping belongs here and nowhere else. Two files translating
|
|
9
|
+
* between the same two vocabularies is how they come to disagree.
|
|
10
|
+
*
|
|
11
|
+
* # What is deliberately NOT here
|
|
12
|
+
*
|
|
13
|
+
* An HTTP client. This package will not carry one even when `@ziffer-io/client` is
|
|
14
|
+
* unavailable: a second implementation of §1 is a second place that knows the
|
|
15
|
+
* auth header, the paths, the status codes and the receipt-passthrough rule,
|
|
16
|
+
* and the second one is always the one that drifts. `docs/onboarding/sdk.md`
|
|
17
|
+
* tells developers there is one client; this package would be the
|
|
18
|
+
* counterexample.
|
|
19
|
+
*
|
|
20
|
+
* There is also no retry, no backoff and no caching. `wait` is the client's own
|
|
21
|
+
* polling loop and it is the one place that decides how often to ask; a second
|
|
22
|
+
* loop here would be two answers to "how hard do we poll", and the tool would
|
|
23
|
+
* be holding a decision the developer's own code has not seen.
|
|
24
|
+
*/
|
|
25
|
+
import { ZifferClient } from '@ziffer-io/client';
|
|
26
|
+
import { apiConfig } from './config.js';
|
|
27
|
+
/**
|
|
28
|
+
* Build a client from the environment, or refuse.
|
|
29
|
+
*
|
|
30
|
+
* Configuration is resolved first and the refusal it raises reaches the agent
|
|
31
|
+
* as the tool's result, so an unset key is reported before any socket is
|
|
32
|
+
* opened. {@link ZifferClient} satisfies {@link DecisionClient} structurally —
|
|
33
|
+
* §6 fixed that surface and `tools.ts` names the two calls used — so there is
|
|
34
|
+
* no adapter here and no cast: had the two disagreed, this is the one file
|
|
35
|
+
* where the mapping would live.
|
|
36
|
+
*/
|
|
37
|
+
export const zifferClientFactory = async (env) => {
|
|
38
|
+
const config = apiConfig(env);
|
|
39
|
+
return new ZifferClient(config.baseUrl, config.apiKey);
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAY,MAAM,aAAa,CAAC;AAGlD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAkB,KAAK,EAAE,GAAQ,EAA2B,EAAE;IAC5F,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;AACzD,CAAC,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the developer hands `npx @ziffer-io/mcp`, and what happens when they have
|
|
3
|
+
* not handed it anything yet (ACP-197, section 6b point 3).
|
|
4
|
+
*
|
|
5
|
+
* This is the only place in this package that reads the environment, for the
|
|
6
|
+
* reason `services/approval/src/config.ts` states one service over: a second
|
|
7
|
+
* `process.env` somewhere in a tool handler would be a second statement of what
|
|
8
|
+
* this server requires, and the one that goes stale is the documented one.
|
|
9
|
+
*
|
|
10
|
+
* # This server starts unconfigured, and that inverts the house rule
|
|
11
|
+
*
|
|
12
|
+
* Every other process in this repository refuses to start without its complete
|
|
13
|
+
* environment. `docs/onboarding/executor.md` section 4 states the reason and it
|
|
14
|
+
* is a good one: a process that came up and cannot serve is worse than one that
|
|
15
|
+
* refused to come up, because a supervisor keeps the first alive.
|
|
16
|
+
*
|
|
17
|
+
* An MCP server over stdio has no supervisor and no operator watching a log. It
|
|
18
|
+
* is spawned by a coding agent, it speaks JSON-RPC on stdout, and when it exits
|
|
19
|
+
* during the initialization handshake the agent is told only that the server
|
|
20
|
+
* failed to start. The variable that was missing died with the process, on a
|
|
21
|
+
* stderr nobody is reading. So the failure would surface to the developer as
|
|
22
|
+
* "the Ziffer MCP server does not work" -- undebuggable from the only side that
|
|
23
|
+
* can fix it.
|
|
24
|
+
*
|
|
25
|
+
* So the process starts, and configuration is resolved PER CALL: each tool that
|
|
26
|
+
* needs a value asks for it, and a missing one is a named {@link ConfigError}
|
|
27
|
+
* returned to the agent as the tool's result, naming the exact variable to set.
|
|
28
|
+
* The agent can then tell its developer, or read
|
|
29
|
+
* {@link https://../../docs/onboarding/sdk.md} through `get_integration_guide`,
|
|
30
|
+
* which is the one tool that deliberately needs no configuration at all.
|
|
31
|
+
*
|
|
32
|
+
* **This is not a relaxation of failing closed.** Nothing proceeds on a missing
|
|
33
|
+
* value: an unconfigured `propose` sends no request and an unconfigured
|
|
34
|
+
* `explain_receipt` verifies nothing. The refusal moved from process exit to
|
|
35
|
+
* tool result; it did not become a default. A default `ZIFFER_API_URL` would
|
|
36
|
+
* point a developer's proposals at a host nobody chose, and a default trust
|
|
37
|
+
* anchor would verify receipts under a key nobody enrolled.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The variables, named once. Every error below quotes one of these, so a rename
|
|
41
|
+
* cannot leave a message pointing at a variable that no longer exists
|
|
42
|
+
* (`services/approval/src/config.ts`'s `VARS`, and `services/kms`'s `pub mod
|
|
43
|
+
* var` one language over).
|
|
44
|
+
*/
|
|
45
|
+
export declare const VARS: {
|
|
46
|
+
readonly API_URL: "ZIFFER_API_URL";
|
|
47
|
+
readonly API_KEY: "ZIFFER_API_KEY";
|
|
48
|
+
readonly TRUST_ANCHOR: "ZIFFER_TRUST_ANCHOR";
|
|
49
|
+
readonly SUITE_FLOOR: "ZIFFER_SUITE_FLOOR";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A configuration value that is missing or is not what it claims to be.
|
|
53
|
+
*
|
|
54
|
+
* `name` is the refusal's machine-readable half, spelled in the PascalCase the
|
|
55
|
+
* ACP-197 section 1 refusals use (`ApiKeyUnknown`, `TenantMismatch`), so an
|
|
56
|
+
* agent can branch on it. `variable` is what the developer has to set, and it
|
|
57
|
+
* is a separate field rather than something to parse back out of the message.
|
|
58
|
+
*
|
|
59
|
+
* `detail` describes the SHAPE of what was wrong and never carries the value.
|
|
60
|
+
* `ZIFFER_API_KEY` is a bearer credential and this text is returned over the
|
|
61
|
+
* MCP transport into an agent's context, which is the last place a live key
|
|
62
|
+
* should be echoed -- the same reason `ApprovalConfig`'s printer redacts its
|
|
63
|
+
* private key one service over.
|
|
64
|
+
*/
|
|
65
|
+
export declare class ConfigError extends Error {
|
|
66
|
+
/** The refusal name an agent branches on. */
|
|
67
|
+
readonly name: string;
|
|
68
|
+
/** The environment variable the developer has to set. */
|
|
69
|
+
readonly variable: string;
|
|
70
|
+
constructor(name: string, variable: string, detail: string);
|
|
71
|
+
}
|
|
72
|
+
/** Where this server sends proposals, and the key that says who is sending. */
|
|
73
|
+
export interface ApiConfig {
|
|
74
|
+
/** `ZIFFER_API_URL` — the gateway's base URL, no trailing slash. */
|
|
75
|
+
readonly baseUrl: string;
|
|
76
|
+
/** `ZIFFER_API_KEY` — the bearer key. It also determines the tenant: ACP-197
|
|
77
|
+
* section 1 makes the KEY the tenant, so this server never sends a tenant
|
|
78
|
+
* name of its own and could not override one if it wanted to. */
|
|
79
|
+
readonly apiKey: string;
|
|
80
|
+
}
|
|
81
|
+
/** Where this server reads the verifier's own trust anchor from. */
|
|
82
|
+
export interface AnchorConfig {
|
|
83
|
+
/** `ZIFFER_TRUST_ANCHOR` — path to an `acp-bundle pubkey` document. */
|
|
84
|
+
readonly anchorPath: string;
|
|
85
|
+
/** `ZIFFER_SUITE_FLOOR` — the CR-4 floor, by wire suite name. */
|
|
86
|
+
readonly suiteFloor: string;
|
|
87
|
+
}
|
|
88
|
+
/** The environment, as a plain map, so tests never mutate `process.env`. */
|
|
89
|
+
export type Env = Readonly<Record<string, string | undefined>>;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the gateway leg, or refuse by name.
|
|
92
|
+
*
|
|
93
|
+
* @throws ConfigError `ApiUrlUnconfigured` or `ApiKeyUnconfigured`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function apiConfig(env: Env): ApiConfig;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the verification leg, or refuse by name.
|
|
98
|
+
*
|
|
99
|
+
* `anchorPathOverride` is `explain_receipt`'s optional argument. It wins over
|
|
100
|
+
* the variable because a developer verifying a receipt against a SECOND
|
|
101
|
+
* identity -- a staging tenant, or a key they are about to rotate to -- should
|
|
102
|
+
* not have to restart their coding agent to do it. When neither is present the
|
|
103
|
+
* refusal names the variable, because that is the durable way to set it.
|
|
104
|
+
*
|
|
105
|
+
* @throws ConfigError `TrustAnchorUnconfigured` or `SuiteFloorUnconfigured`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function anchorConfig(env: Env, anchorPathOverride?: string): AnchorConfig;
|
|
108
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,IAAI;;;;;CAKP,CAAC;AAEX;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,6CAA6C;IAC7C,SAAkB,IAAI,EAAE,MAAM,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK3D;AAED,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;qEAEiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,4EAA4E;AAC5E,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAU/D;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,CA2C7C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,YAAY,CAkBhF"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the developer hands `npx @ziffer-io/mcp`, and what happens when they have
|
|
3
|
+
* not handed it anything yet (ACP-197, section 6b point 3).
|
|
4
|
+
*
|
|
5
|
+
* This is the only place in this package that reads the environment, for the
|
|
6
|
+
* reason `services/approval/src/config.ts` states one service over: a second
|
|
7
|
+
* `process.env` somewhere in a tool handler would be a second statement of what
|
|
8
|
+
* this server requires, and the one that goes stale is the documented one.
|
|
9
|
+
*
|
|
10
|
+
* # This server starts unconfigured, and that inverts the house rule
|
|
11
|
+
*
|
|
12
|
+
* Every other process in this repository refuses to start without its complete
|
|
13
|
+
* environment. `docs/onboarding/executor.md` section 4 states the reason and it
|
|
14
|
+
* is a good one: a process that came up and cannot serve is worse than one that
|
|
15
|
+
* refused to come up, because a supervisor keeps the first alive.
|
|
16
|
+
*
|
|
17
|
+
* An MCP server over stdio has no supervisor and no operator watching a log. It
|
|
18
|
+
* is spawned by a coding agent, it speaks JSON-RPC on stdout, and when it exits
|
|
19
|
+
* during the initialization handshake the agent is told only that the server
|
|
20
|
+
* failed to start. The variable that was missing died with the process, on a
|
|
21
|
+
* stderr nobody is reading. So the failure would surface to the developer as
|
|
22
|
+
* "the Ziffer MCP server does not work" -- undebuggable from the only side that
|
|
23
|
+
* can fix it.
|
|
24
|
+
*
|
|
25
|
+
* So the process starts, and configuration is resolved PER CALL: each tool that
|
|
26
|
+
* needs a value asks for it, and a missing one is a named {@link ConfigError}
|
|
27
|
+
* returned to the agent as the tool's result, naming the exact variable to set.
|
|
28
|
+
* The agent can then tell its developer, or read
|
|
29
|
+
* {@link https://../../docs/onboarding/sdk.md} through `get_integration_guide`,
|
|
30
|
+
* which is the one tool that deliberately needs no configuration at all.
|
|
31
|
+
*
|
|
32
|
+
* **This is not a relaxation of failing closed.** Nothing proceeds on a missing
|
|
33
|
+
* value: an unconfigured `propose` sends no request and an unconfigured
|
|
34
|
+
* `explain_receipt` verifies nothing. The refusal moved from process exit to
|
|
35
|
+
* tool result; it did not become a default. A default `ZIFFER_API_URL` would
|
|
36
|
+
* point a developer's proposals at a host nobody chose, and a default trust
|
|
37
|
+
* anchor would verify receipts under a key nobody enrolled.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The variables, named once. Every error below quotes one of these, so a rename
|
|
41
|
+
* cannot leave a message pointing at a variable that no longer exists
|
|
42
|
+
* (`services/approval/src/config.ts`'s `VARS`, and `services/kms`'s `pub mod
|
|
43
|
+
* var` one language over).
|
|
44
|
+
*/
|
|
45
|
+
export const VARS = {
|
|
46
|
+
API_URL: 'ZIFFER_API_URL',
|
|
47
|
+
API_KEY: 'ZIFFER_API_KEY',
|
|
48
|
+
TRUST_ANCHOR: 'ZIFFER_TRUST_ANCHOR',
|
|
49
|
+
SUITE_FLOOR: 'ZIFFER_SUITE_FLOOR',
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A configuration value that is missing or is not what it claims to be.
|
|
53
|
+
*
|
|
54
|
+
* `name` is the refusal's machine-readable half, spelled in the PascalCase the
|
|
55
|
+
* ACP-197 section 1 refusals use (`ApiKeyUnknown`, `TenantMismatch`), so an
|
|
56
|
+
* agent can branch on it. `variable` is what the developer has to set, and it
|
|
57
|
+
* is a separate field rather than something to parse back out of the message.
|
|
58
|
+
*
|
|
59
|
+
* `detail` describes the SHAPE of what was wrong and never carries the value.
|
|
60
|
+
* `ZIFFER_API_KEY` is a bearer credential and this text is returned over the
|
|
61
|
+
* MCP transport into an agent's context, which is the last place a live key
|
|
62
|
+
* should be echoed -- the same reason `ApprovalConfig`'s printer redacts its
|
|
63
|
+
* private key one service over.
|
|
64
|
+
*/
|
|
65
|
+
export class ConfigError extends Error {
|
|
66
|
+
/** The refusal name an agent branches on. */
|
|
67
|
+
name;
|
|
68
|
+
/** The environment variable the developer has to set. */
|
|
69
|
+
variable;
|
|
70
|
+
constructor(name, variable, detail) {
|
|
71
|
+
super(`${name}: ${detail} Set ${variable}.`);
|
|
72
|
+
this.name = name;
|
|
73
|
+
this.variable = variable;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function required(env, variable, refusal, detail) {
|
|
77
|
+
const raw = env[variable];
|
|
78
|
+
if (raw === undefined || raw.trim() === '') {
|
|
79
|
+
throw new ConfigError(refusal, variable, detail);
|
|
80
|
+
}
|
|
81
|
+
return raw.trim();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the gateway leg, or refuse by name.
|
|
85
|
+
*
|
|
86
|
+
* @throws ConfigError `ApiUrlUnconfigured` or `ApiKeyUnconfigured`.
|
|
87
|
+
*/
|
|
88
|
+
export function apiConfig(env) {
|
|
89
|
+
const raw = required(env, VARS.API_URL, 'ApiUrlUnconfigured', 'this server does not know which Ziffer gateway to call.');
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(raw);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Named separately from the unset case because they are different developer
|
|
96
|
+
// actions: one is "you have set nothing", the other is "what you set is not
|
|
97
|
+
// a URL". A single refusal covering both sends the reader to check a
|
|
98
|
+
// variable that is, in fact, present.
|
|
99
|
+
throw new ConfigError('ApiUrlMalformed', VARS.API_URL, 'the value is not an absolute URL (expected something like https://api.ziffer.io).');
|
|
100
|
+
}
|
|
101
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
102
|
+
// The API key is a bearer credential, so plaintext to a remote host hands
|
|
103
|
+
// it to anyone on the path. Localhost is exempt because the stub server the
|
|
104
|
+
// tests run, and the local gateway a developer runs while integrating, are
|
|
105
|
+
// both http:// on the loopback -- refusing those would mean the only way to
|
|
106
|
+
// try this package is against production.
|
|
107
|
+
throw new ConfigError('ApiUrlInsecure', VARS.API_URL, `the value is ${parsed.protocol}// to a remote host, which would send the bearer key in plaintext; use https:// (http:// is allowed only for localhost).`);
|
|
108
|
+
}
|
|
109
|
+
const apiKey = required(env, VARS.API_KEY, 'ApiKeyUnconfigured', 'this server has no Ziffer API key, so it cannot say which tenant is proposing.');
|
|
110
|
+
// No trailing slash, once, here: a base URL that sometimes ends in one turns
|
|
111
|
+
// every join site into a place where `//v1/proposals` can be built, and the
|
|
112
|
+
// gateway would answer that with a 404 that names nothing.
|
|
113
|
+
return { baseUrl: raw.replace(/\/+$/, ''), apiKey };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the verification leg, or refuse by name.
|
|
117
|
+
*
|
|
118
|
+
* `anchorPathOverride` is `explain_receipt`'s optional argument. It wins over
|
|
119
|
+
* the variable because a developer verifying a receipt against a SECOND
|
|
120
|
+
* identity -- a staging tenant, or a key they are about to rotate to -- should
|
|
121
|
+
* not have to restart their coding agent to do it. When neither is present the
|
|
122
|
+
* refusal names the variable, because that is the durable way to set it.
|
|
123
|
+
*
|
|
124
|
+
* @throws ConfigError `TrustAnchorUnconfigured` or `SuiteFloorUnconfigured`.
|
|
125
|
+
*/
|
|
126
|
+
export function anchorConfig(env, anchorPathOverride) {
|
|
127
|
+
const override = anchorPathOverride?.trim();
|
|
128
|
+
const anchorPath = override !== undefined && override !== ''
|
|
129
|
+
? override
|
|
130
|
+
: required(env, VARS.TRUST_ANCHOR, 'TrustAnchorUnconfigured', 'this server has no trust anchor, so it has no identity to verify receipts under.');
|
|
131
|
+
const suiteFloor = required(env, VARS.SUITE_FLOOR, 'SuiteFloorUnconfigured', 'this server has no CR-4 suite floor, and there is no default one: a floor chosen here would be a minimum signature strength nobody agreed to.');
|
|
132
|
+
return { anchorPath, suiteFloor };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|