@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/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
@@ -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"}
@@ -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"}
@@ -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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"}