@crediolabs/policy-builder-mcp 0.5.6 → 1.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/src/schemas.d.ts +101 -6
- package/dist/src/schemas.js +58 -2
- package/dist/src/server.js +9 -5
- package/dist-cjs/src/schemas.d.ts +101 -6
- package/dist-cjs/src/schemas.js +58 -2
- package/dist-cjs/src/server.js +9 -5
- package/package.json +2 -2
- package/src/schemas.ts +58 -2
- package/src/server.ts +10 -7
package/dist/src/schemas.d.ts
CHANGED
|
@@ -274,6 +274,7 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
274
274
|
}>;
|
|
275
275
|
sourceAccount: z.ZodString;
|
|
276
276
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
277
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
277
278
|
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
278
279
|
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
279
280
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
@@ -308,8 +309,9 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
308
309
|
readonly explain: z.ZodOptional<z.ZodBoolean>;
|
|
309
310
|
};
|
|
310
311
|
export declare const SimulatePolicyToolShape: {
|
|
311
|
-
readonly predicate: z.ZodType<unknown, z.ZodTypeDef, unknown
|
|
312
|
-
readonly
|
|
312
|
+
readonly predicate: z.ZodOptional<z.ZodType<unknown, z.ZodTypeDef, unknown>>;
|
|
313
|
+
readonly encodedPredicate: z.ZodOptional<z.ZodString>;
|
|
314
|
+
readonly permitTx: z.ZodOptional<z.ZodObject<{
|
|
313
315
|
network: z.ZodEnum<["mainnet", "testnet"]>;
|
|
314
316
|
signers: z.ZodArray<z.ZodString, "many">;
|
|
315
317
|
invocations: z.ZodArray<z.ZodType<unknown, z.ZodTypeDef, unknown>, "many">;
|
|
@@ -567,12 +569,29 @@ export declare const SimulatePolicyToolShape: {
|
|
|
567
569
|
thresholdUsed: number;
|
|
568
570
|
}>;
|
|
569
571
|
sourceAccount: z.ZodString;
|
|
570
|
-
}, z.ZodTypeAny, "passthrough"
|
|
572
|
+
}, z.ZodTypeAny, "passthrough">>>;
|
|
573
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
574
|
+
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
575
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
576
|
+
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
577
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
578
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
579
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
580
|
+
}, "strict", z.ZodTypeAny, {
|
|
581
|
+
limitAmount?: string | undefined;
|
|
582
|
+
validUntilLedger?: number | undefined;
|
|
583
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
584
|
+
}, {
|
|
585
|
+
limitAmount?: string | undefined;
|
|
586
|
+
validUntilLedger?: number | undefined;
|
|
587
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
588
|
+
}>>;
|
|
571
589
|
readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
572
590
|
};
|
|
573
591
|
export declare const VerifyPolicyToolShape: {
|
|
574
|
-
readonly predicate: z.ZodType<unknown, z.ZodTypeDef, unknown
|
|
575
|
-
readonly
|
|
592
|
+
readonly predicate: z.ZodOptional<z.ZodType<unknown, z.ZodTypeDef, unknown>>;
|
|
593
|
+
readonly encodedPredicate: z.ZodOptional<z.ZodString>;
|
|
594
|
+
readonly permitTx: z.ZodOptional<z.ZodObject<{
|
|
576
595
|
network: z.ZodEnum<["mainnet", "testnet"]>;
|
|
577
596
|
signers: z.ZodArray<z.ZodString, "many">;
|
|
578
597
|
invocations: z.ZodArray<z.ZodType<unknown, z.ZodTypeDef, unknown>, "many">;
|
|
@@ -830,7 +849,23 @@ export declare const VerifyPolicyToolShape: {
|
|
|
830
849
|
thresholdUsed: number;
|
|
831
850
|
}>;
|
|
832
851
|
sourceAccount: z.ZodString;
|
|
833
|
-
}, z.ZodTypeAny, "passthrough"
|
|
852
|
+
}, z.ZodTypeAny, "passthrough">>>;
|
|
853
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
854
|
+
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
855
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
856
|
+
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
857
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
858
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
859
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
860
|
+
}, "strict", z.ZodTypeAny, {
|
|
861
|
+
limitAmount?: string | undefined;
|
|
862
|
+
validUntilLedger?: number | undefined;
|
|
863
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
864
|
+
}, {
|
|
865
|
+
limitAmount?: string | undefined;
|
|
866
|
+
validUntilLedger?: number | undefined;
|
|
867
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
868
|
+
}>>;
|
|
834
869
|
readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
835
870
|
};
|
|
836
871
|
export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, SimulatePolicyInput, VerifyPolicyInput, } from '@crediolabs/policy-synth/run';
|
|
@@ -891,7 +926,67 @@ export declare const InstallPolicyToolShape: {
|
|
|
891
926
|
* "not checked" rather than "nothing found". */
|
|
892
927
|
readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
893
928
|
readonly rule: z.ZodOptional<z.ZodUnknown>;
|
|
929
|
+
readonly fromPredicate: z.ZodOptional<z.ZodObject<{
|
|
930
|
+
encodedPredicate: z.ZodString;
|
|
931
|
+
signers: z.ZodArray<z.ZodString, "many">;
|
|
932
|
+
name: z.ZodOptional<z.ZodString>;
|
|
933
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
934
|
+
}, "strip", z.ZodTypeAny, {
|
|
935
|
+
signers: string[];
|
|
936
|
+
encodedPredicate: string;
|
|
937
|
+
name?: string | undefined;
|
|
938
|
+
validUntilLedger?: number | undefined;
|
|
939
|
+
}, {
|
|
940
|
+
signers: string[];
|
|
941
|
+
encodedPredicate: string;
|
|
942
|
+
name?: string | undefined;
|
|
943
|
+
validUntilLedger?: number | undefined;
|
|
944
|
+
}>>;
|
|
945
|
+
readonly fromHash: z.ZodOptional<z.ZodObject<{
|
|
946
|
+
transactionHash: z.ZodString;
|
|
947
|
+
signers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
948
|
+
userResponses: z.ZodOptional<z.ZodObject<{
|
|
949
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
950
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
951
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
952
|
+
}, "strict", z.ZodTypeAny, {
|
|
953
|
+
limitAmount?: string | undefined;
|
|
954
|
+
validUntilLedger?: number | undefined;
|
|
955
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
956
|
+
}, {
|
|
957
|
+
limitAmount?: string | undefined;
|
|
958
|
+
validUntilLedger?: number | undefined;
|
|
959
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
960
|
+
}>>;
|
|
961
|
+
}, "strip", z.ZodTypeAny, {
|
|
962
|
+
transactionHash: string;
|
|
963
|
+
signers?: string[] | undefined;
|
|
964
|
+
userResponses?: {
|
|
965
|
+
limitAmount?: string | undefined;
|
|
966
|
+
validUntilLedger?: number | undefined;
|
|
967
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
968
|
+
} | undefined;
|
|
969
|
+
}, {
|
|
970
|
+
transactionHash: string;
|
|
971
|
+
signers?: string[] | undefined;
|
|
972
|
+
userResponses?: {
|
|
973
|
+
limitAmount?: string | undefined;
|
|
974
|
+
validUntilLedger?: number | undefined;
|
|
975
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
976
|
+
} | undefined;
|
|
977
|
+
}>>;
|
|
894
978
|
readonly installNonce: z.ZodOptional<z.ZodNumber>;
|
|
979
|
+
readonly spendingLimit: z.ZodOptional<z.ZodObject<{
|
|
980
|
+
amount: z.ZodString;
|
|
981
|
+
periodLedgers: z.ZodNumber;
|
|
982
|
+
}, "strip", z.ZodTypeAny, {
|
|
983
|
+
amount: string;
|
|
984
|
+
periodLedgers: number;
|
|
985
|
+
}, {
|
|
986
|
+
amount: string;
|
|
987
|
+
periodLedgers: number;
|
|
988
|
+
}>>;
|
|
989
|
+
readonly allowUnboundedAmount: z.ZodOptional<z.ZodBoolean>;
|
|
895
990
|
readonly interpreterAddress: z.ZodOptional<z.ZodString>;
|
|
896
991
|
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
897
992
|
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
package/dist/src/schemas.js
CHANGED
|
@@ -42,7 +42,12 @@ export const RecordTransactionToolShape = {
|
|
|
42
42
|
* re-validates against the strict schema. */
|
|
43
43
|
export const SynthesizePolicyToolShape = {
|
|
44
44
|
source: z.literal('recording').optional(),
|
|
45
|
+
// Supply EITHER `recordedTx` (the whole recording) OR `transactionHash`. An agent
|
|
46
|
+
// should prefer the hash: it cannot copy a multi-thousand-character recording back out
|
|
47
|
+
// of its own transcript without losing fields, and the server can re-record
|
|
48
|
+
// from the hash deterministically.
|
|
45
49
|
recordedTx: RecordedTransactionSchema.optional(),
|
|
50
|
+
transactionHash: z.string().optional(),
|
|
46
51
|
network: NetworkSchema.optional(),
|
|
47
52
|
userResponses: ComposeUserResponsesSchema.optional(),
|
|
48
53
|
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
@@ -60,8 +65,21 @@ export const SynthesizePolicyToolShape = {
|
|
|
60
65
|
* (from `synthesize_policy` under `explain`) plus the recording it was
|
|
61
66
|
* synthesised from. */
|
|
62
67
|
const PolicyCheckToolShape = {
|
|
63
|
-
predicate
|
|
64
|
-
|
|
68
|
+
// Supply EITHER `transactionHash` OR both `predicate` and `permitTx`. The tree comes
|
|
69
|
+
// back from `synthesize_policy` only under `explain`, so a caller who did not
|
|
70
|
+
// ask for it has nothing to pass and would skip the check - and this IS the
|
|
71
|
+
// check. `transactionHash` lets the server rebuild both from the transaction.
|
|
72
|
+
predicate: PredicateNodeSchema.optional(),
|
|
73
|
+
// The canonical encoding `declare_policy` and `synthesize_policy` both
|
|
74
|
+
// return. One opaque string, where the tree is what callers mistype.
|
|
75
|
+
encodedPredicate: z.string().optional(),
|
|
76
|
+
permitTx: RecordedTransactionSchema.optional(),
|
|
77
|
+
transactionHash: z.string().optional(),
|
|
78
|
+
network: NetworkSchema.optional(),
|
|
79
|
+
// Needed with `transactionHash`: the predicate is lowered against the account it will
|
|
80
|
+
// be installed on.
|
|
81
|
+
smartAccount: z.string().optional(),
|
|
82
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
65
83
|
validUntilLedger: z.number().int().positive().optional(),
|
|
66
84
|
};
|
|
67
85
|
export const SimulatePolicyToolShape = { ...PolicyCheckToolShape };
|
|
@@ -127,8 +145,46 @@ export const InstallPolicyToolShape = {
|
|
|
127
145
|
* authority scan; omitting them returns `authorityScan: null`, which means
|
|
128
146
|
* "not checked" rather than "nothing found". */
|
|
129
147
|
existingRules: z.array(z.unknown()).optional(),
|
|
148
|
+
// Supply EITHER `rule` (the whole ContextRuleDraft) OR `fromHash`. Same
|
|
149
|
+
// reason as `synthesize_policy`: a draft nests i128 strings, a signer array
|
|
150
|
+
// and a policy array, and an agent retyping it out of its own transcript
|
|
151
|
+
// loses their types. `fromHash` re-derives the draft server-side from the
|
|
152
|
+
// recording, so the install runs against the rule the synthesizer produced.
|
|
130
153
|
rule: z.unknown().optional(),
|
|
154
|
+
// Install a predicate you already hold - the base64 string `declare_policy`
|
|
155
|
+
// returns - naming the keys it governs as plain account addresses. Without
|
|
156
|
+
// this there is no route from `declare_policy` to here.
|
|
157
|
+
fromPredicate: z
|
|
158
|
+
.object({
|
|
159
|
+
encodedPredicate: z.string(),
|
|
160
|
+
signers: z.array(z.string()),
|
|
161
|
+
name: z.string().optional(),
|
|
162
|
+
validUntilLedger: z.number().int().positive().optional(),
|
|
163
|
+
})
|
|
164
|
+
.optional(),
|
|
165
|
+
fromHash: z
|
|
166
|
+
.object({
|
|
167
|
+
transactionHash: z.string(),
|
|
168
|
+
// The keys the rule governs, as plain Stellar addresses. Synthesis does
|
|
169
|
+
// not choose them, and a rule governing no key is refused on chain.
|
|
170
|
+
signers: z.array(z.string()).optional(),
|
|
171
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
172
|
+
})
|
|
173
|
+
.optional(),
|
|
131
174
|
installNonce: z.number().int().positive().optional(),
|
|
175
|
+
// A ROLLING TOTAL beside the per-call predicate, as an OpenZeppelin
|
|
176
|
+
// spending_limit on the same rule. Both must permit. This is the only way to
|
|
177
|
+
// express "N per day": the interpreter is handed one call and keeps no state.
|
|
178
|
+
spendingLimit: z
|
|
179
|
+
.object({
|
|
180
|
+
amount: z.string(),
|
|
181
|
+
periodLedgers: z.number().int().positive(),
|
|
182
|
+
})
|
|
183
|
+
.optional(),
|
|
184
|
+
// Opt-in to installing a rule that bounds no amount when the recording showed
|
|
185
|
+
// a spend. Default-deny: such a rule installs and verifies cleanly and caps
|
|
186
|
+
// nothing.
|
|
187
|
+
allowUnboundedAmount: z.boolean().optional(),
|
|
132
188
|
interpreterAddress: z.string().optional(),
|
|
133
189
|
};
|
|
134
190
|
/** Flat ZodRawShape for `revoke_policy`. */
|
package/dist/src/server.js
CHANGED
|
@@ -22,18 +22,22 @@ const toCallToolResult = (res) => mcpResultFromCore(res);
|
|
|
22
22
|
/** Build a fresh, stateless MCP server. The caller owns the returned object
|
|
23
23
|
* and connects it to a single transport (stdio or Streamable HTTP). */
|
|
24
24
|
export function createMcpServer() {
|
|
25
|
-
const server = new McpServer(
|
|
25
|
+
const server = new McpServer(
|
|
26
|
+
// Kept in step with package.json by `server-version.test.ts`. It was
|
|
27
|
+
// hardcoded once already and reported 0.0.0 to every client; a literal is
|
|
28
|
+
// fine, a literal nothing checks is not.
|
|
29
|
+
{ name: 'policy-builder-mcp', version: '1.1.0' }, { capabilities: { tools: {} } });
|
|
26
30
|
registerTools(server);
|
|
27
31
|
return server;
|
|
28
32
|
}
|
|
29
33
|
/** Idempotent registration of the T1 tool set on the given server. */
|
|
30
34
|
export function registerTools(server) {
|
|
31
35
|
server.tool('record_transaction', 'Decode a Soroban transaction (on-chain hash OR base64 envelope XDR) into a RecordedTransaction. Returns a machine-readable ToolError on validation failure.', RecordTransactionToolShape, (args) => runRecordTransaction(args).then(toCallToolResult));
|
|
32
|
-
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a
|
|
33
|
-
server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass the
|
|
34
|
-
server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose.', VerifyPolicyToolShape, (args) =>
|
|
36
|
+
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a recorded transaction (`source: recording`). Pass `transactionHash` and `network` to have the server re-record and synthesize in one step - prefer this over copying a `recordedTx` payload back out of a previous tool result, which loses fields. `recordedTx` remains accepted for programmatic callers holding the object.', SynthesizePolicyToolShape, (args) => runSynthesizePolicy(args).then(toCallToolResult));
|
|
37
|
+
server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass `transactionHash` with `smartAccount` and the server records and synthesizes the predicate itself; or pass a predicate together with the call to check it against. For a predicate you already hold, pass `encodedPredicate` (the base64 string `declare_policy` and `synthesize_policy` return) rather than retyping the `predicate` tree.', SimulatePolicyToolShape, (args) => runSimulatePolicy(args).then(toCallToolResult));
|
|
38
|
+
server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose. Pass `transactionHash` with `smartAccount` and the server rebuilds the predicate and the recording itself; For a predicate you already hold - a DECLARED one has no recording behind it, so re-synthesizing would check something else - pass `encodedPredicate` (the base64 string) with `transactionHash`. Retyping the `predicate` tree is the error-prone path; do not take it, and never skip this check because the tree was not to hand.', VerifyPolicyToolShape, (args) => runVerifyPolicy(args).then(toCallToolResult));
|
|
35
39
|
server.tool('declare_policy', "Build an interpreter predicate from a DECLARED constraint - the method to pin, and optionally the contract, a per-call amount cap, a recipient allowlist, and a minimum-output ratio (a swap slippage floor, bounding the output argument against the call's own input). Use this when there is no transaction to record, or when `record_transaction` refuses a contract it does not recognise. Returns the predicate tree, its canonical encoding and hash, ready for `install_policy`. `warnings` names any argument index that was GUESSED rather than supplied - a bound on the wrong argument constrains nothing while looking correct, so read them. There is no rolling spend window and no approval threshold: neither is expressible in grammar 4, and both belong to the OpenZeppelin account layer.", DeclarePolicyToolShape, (args) => Promise.resolve(runDeclarePolicy(args)).then(toCallToolResult));
|
|
36
|
-
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair).
|
|
40
|
+
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. Name the rule ONE of three ways. `fromPredicate: { encodedPredicate, signers }` installs a predicate you already hold - the base64 string `declare_policy` returns - and scopes the rule to whatever contract that predicate pins. `fromHash: { transactionHash, signers }` - with `fromHash` the server re-synthesizes the rule from the recording itself, and `signers` (plain G.../C... addresses) names the keys the rule governs. Naming them is required: synthesis reads a transaction and cannot decide which keys a rule binds, and a rule that governs no key is refused on chain. `rule` is the full ContextRuleDraft and is for programmatic callers only - retyping it loses the types of `validUntilLedger`, `signers` and `policies`. In BOTH handle forms `signers` is a list of plain Stellar account addresses (G...): a delegated signer is an account, NOT a deployed signer contract, and nothing needs to be deployed to name one. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair). `installNonce` defaults to 1, which is correct for a fresh rule - do not ask the caller for it. If the recorded call spends an amount and no `limitAmount` is given, the install is REFUSED: such a rule would constrain everything about the call except how much it moves, and it would install and verify cleanly while capping nothing. Supply `fromHash.userResponses.limitAmount`, or `allowUnboundedAmount: true` to do it deliberately. For a ROLLING TOTAL - "at most N per day" - pass `spendingLimit: { amount, periodLedgers }`. It attaches an OpenZeppelin `spending_limit` beside the predicate on the same rule, and both must permit. A predicate alone CANNOT express a rolling total: the interpreter is handed one call and keeps no state, so a per-call cap of N authorises N again on the very next call. The window counts LEDGERS (about five seconds each), and the rule must be scoped to the token contract whose transfers it meters. One transaction is all it takes: the smart account calls the interpreter `install` itself while running `add_context_rule`, so once this XDR is signed and submitted the rule is live and the predicate is enforced. The rule id the account assigned is in the transaction result. Do not tell the caller a second call is outstanding.', InstallPolicyToolShape, (args) => runInstallPolicy(args).then(toCallToolResult));
|
|
37
41
|
server.tool('revoke_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.remove_context_rule(ruleId)` that removes a policy rule from the given smart account. The smart account handles uninstalling each attached policy itself. Auth is master-only - the source account MUST be the master signer set; delegated signers cannot uninstall.', RevokePolicyToolShape, (args) => runRevokePolicy(args).then(toCallToolResult));
|
|
38
42
|
server.tool('get_interpreter_info', 'Read-only fingerprint lookup for the policy interpreter contract: returns the pinned address, grammar version, and wasm sha256 (from the pinned constants + SELF_VERSION). When `verifyLive=true`, performs an additional `grammar_version()` RPC call against the pinned address and reports whether the deployed contract matches the pin - a live mismatch check is more useful than a fabricated audit field.', GetInterpreterInfoToolShape, (args) => runGetInterpreterInfo(args).then(toCallToolResult));
|
|
39
43
|
}
|
|
@@ -274,6 +274,7 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
274
274
|
}>;
|
|
275
275
|
sourceAccount: z.ZodString;
|
|
276
276
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
277
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
277
278
|
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
278
279
|
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
279
280
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
@@ -308,8 +309,9 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
308
309
|
readonly explain: z.ZodOptional<z.ZodBoolean>;
|
|
309
310
|
};
|
|
310
311
|
export declare const SimulatePolicyToolShape: {
|
|
311
|
-
readonly predicate: z.ZodType<unknown, z.ZodTypeDef, unknown
|
|
312
|
-
readonly
|
|
312
|
+
readonly predicate: z.ZodOptional<z.ZodType<unknown, z.ZodTypeDef, unknown>>;
|
|
313
|
+
readonly encodedPredicate: z.ZodOptional<z.ZodString>;
|
|
314
|
+
readonly permitTx: z.ZodOptional<z.ZodObject<{
|
|
313
315
|
network: z.ZodEnum<["mainnet", "testnet"]>;
|
|
314
316
|
signers: z.ZodArray<z.ZodString, "many">;
|
|
315
317
|
invocations: z.ZodArray<z.ZodType<unknown, z.ZodTypeDef, unknown>, "many">;
|
|
@@ -567,12 +569,29 @@ export declare const SimulatePolicyToolShape: {
|
|
|
567
569
|
thresholdUsed: number;
|
|
568
570
|
}>;
|
|
569
571
|
sourceAccount: z.ZodString;
|
|
570
|
-
}, z.ZodTypeAny, "passthrough"
|
|
572
|
+
}, z.ZodTypeAny, "passthrough">>>;
|
|
573
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
574
|
+
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
575
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
576
|
+
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
577
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
578
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
579
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
580
|
+
}, "strict", z.ZodTypeAny, {
|
|
581
|
+
limitAmount?: string | undefined;
|
|
582
|
+
validUntilLedger?: number | undefined;
|
|
583
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
584
|
+
}, {
|
|
585
|
+
limitAmount?: string | undefined;
|
|
586
|
+
validUntilLedger?: number | undefined;
|
|
587
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
588
|
+
}>>;
|
|
571
589
|
readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
572
590
|
};
|
|
573
591
|
export declare const VerifyPolicyToolShape: {
|
|
574
|
-
readonly predicate: z.ZodType<unknown, z.ZodTypeDef, unknown
|
|
575
|
-
readonly
|
|
592
|
+
readonly predicate: z.ZodOptional<z.ZodType<unknown, z.ZodTypeDef, unknown>>;
|
|
593
|
+
readonly encodedPredicate: z.ZodOptional<z.ZodString>;
|
|
594
|
+
readonly permitTx: z.ZodOptional<z.ZodObject<{
|
|
576
595
|
network: z.ZodEnum<["mainnet", "testnet"]>;
|
|
577
596
|
signers: z.ZodArray<z.ZodString, "many">;
|
|
578
597
|
invocations: z.ZodArray<z.ZodType<unknown, z.ZodTypeDef, unknown>, "many">;
|
|
@@ -830,7 +849,23 @@ export declare const VerifyPolicyToolShape: {
|
|
|
830
849
|
thresholdUsed: number;
|
|
831
850
|
}>;
|
|
832
851
|
sourceAccount: z.ZodString;
|
|
833
|
-
}, z.ZodTypeAny, "passthrough"
|
|
852
|
+
}, z.ZodTypeAny, "passthrough">>>;
|
|
853
|
+
readonly transactionHash: z.ZodOptional<z.ZodString>;
|
|
854
|
+
readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
|
855
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
856
|
+
readonly userResponses: z.ZodOptional<z.ZodObject<{
|
|
857
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
858
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
859
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
860
|
+
}, "strict", z.ZodTypeAny, {
|
|
861
|
+
limitAmount?: string | undefined;
|
|
862
|
+
validUntilLedger?: number | undefined;
|
|
863
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
864
|
+
}, {
|
|
865
|
+
limitAmount?: string | undefined;
|
|
866
|
+
validUntilLedger?: number | undefined;
|
|
867
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
868
|
+
}>>;
|
|
834
869
|
readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
835
870
|
};
|
|
836
871
|
export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, SimulatePolicyInput, VerifyPolicyInput, } from '@crediolabs/policy-synth/run';
|
|
@@ -891,7 +926,67 @@ export declare const InstallPolicyToolShape: {
|
|
|
891
926
|
* "not checked" rather than "nothing found". */
|
|
892
927
|
readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
893
928
|
readonly rule: z.ZodOptional<z.ZodUnknown>;
|
|
929
|
+
readonly fromPredicate: z.ZodOptional<z.ZodObject<{
|
|
930
|
+
encodedPredicate: z.ZodString;
|
|
931
|
+
signers: z.ZodArray<z.ZodString, "many">;
|
|
932
|
+
name: z.ZodOptional<z.ZodString>;
|
|
933
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
934
|
+
}, "strip", z.ZodTypeAny, {
|
|
935
|
+
signers: string[];
|
|
936
|
+
encodedPredicate: string;
|
|
937
|
+
name?: string | undefined;
|
|
938
|
+
validUntilLedger?: number | undefined;
|
|
939
|
+
}, {
|
|
940
|
+
signers: string[];
|
|
941
|
+
encodedPredicate: string;
|
|
942
|
+
name?: string | undefined;
|
|
943
|
+
validUntilLedger?: number | undefined;
|
|
944
|
+
}>>;
|
|
945
|
+
readonly fromHash: z.ZodOptional<z.ZodObject<{
|
|
946
|
+
transactionHash: z.ZodString;
|
|
947
|
+
signers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
948
|
+
userResponses: z.ZodOptional<z.ZodObject<{
|
|
949
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
950
|
+
limitAmount: z.ZodOptional<z.ZodString>;
|
|
951
|
+
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
952
|
+
}, "strict", z.ZodTypeAny, {
|
|
953
|
+
limitAmount?: string | undefined;
|
|
954
|
+
validUntilLedger?: number | undefined;
|
|
955
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
956
|
+
}, {
|
|
957
|
+
limitAmount?: string | undefined;
|
|
958
|
+
validUntilLedger?: number | undefined;
|
|
959
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
960
|
+
}>>;
|
|
961
|
+
}, "strip", z.ZodTypeAny, {
|
|
962
|
+
transactionHash: string;
|
|
963
|
+
signers?: string[] | undefined;
|
|
964
|
+
userResponses?: {
|
|
965
|
+
limitAmount?: string | undefined;
|
|
966
|
+
validUntilLedger?: number | undefined;
|
|
967
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
968
|
+
} | undefined;
|
|
969
|
+
}, {
|
|
970
|
+
transactionHash: string;
|
|
971
|
+
signers?: string[] | undefined;
|
|
972
|
+
userResponses?: {
|
|
973
|
+
limitAmount?: string | undefined;
|
|
974
|
+
validUntilLedger?: number | undefined;
|
|
975
|
+
swapRecipientAllowlist?: string[] | undefined;
|
|
976
|
+
} | undefined;
|
|
977
|
+
}>>;
|
|
894
978
|
readonly installNonce: z.ZodOptional<z.ZodNumber>;
|
|
979
|
+
readonly spendingLimit: z.ZodOptional<z.ZodObject<{
|
|
980
|
+
amount: z.ZodString;
|
|
981
|
+
periodLedgers: z.ZodNumber;
|
|
982
|
+
}, "strip", z.ZodTypeAny, {
|
|
983
|
+
amount: string;
|
|
984
|
+
periodLedgers: number;
|
|
985
|
+
}, {
|
|
986
|
+
amount: string;
|
|
987
|
+
periodLedgers: number;
|
|
988
|
+
}>>;
|
|
989
|
+
readonly allowUnboundedAmount: z.ZodOptional<z.ZodBoolean>;
|
|
895
990
|
readonly interpreterAddress: z.ZodOptional<z.ZodString>;
|
|
896
991
|
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
897
992
|
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
package/dist-cjs/src/schemas.js
CHANGED
|
@@ -50,7 +50,12 @@ exports.RecordTransactionToolShape = {
|
|
|
50
50
|
* re-validates against the strict schema. */
|
|
51
51
|
exports.SynthesizePolicyToolShape = {
|
|
52
52
|
source: zod_1.z.literal('recording').optional(),
|
|
53
|
+
// Supply EITHER `recordedTx` (the whole recording) OR `transactionHash`. An agent
|
|
54
|
+
// should prefer the hash: it cannot copy a multi-thousand-character recording back out
|
|
55
|
+
// of its own transcript without losing fields, and the server can re-record
|
|
56
|
+
// from the hash deterministically.
|
|
53
57
|
recordedTx: run_1.RecordedTransactionSchema.optional(),
|
|
58
|
+
transactionHash: zod_1.z.string().optional(),
|
|
54
59
|
network: run_1.NetworkSchema.optional(),
|
|
55
60
|
userResponses: run_1.ComposeUserResponsesSchema.optional(),
|
|
56
61
|
confidenceOverride: zod_1.z.object({ threshold: zod_1.z.number().min(0).max(1) }).optional(),
|
|
@@ -68,8 +73,21 @@ exports.SynthesizePolicyToolShape = {
|
|
|
68
73
|
* (from `synthesize_policy` under `explain`) plus the recording it was
|
|
69
74
|
* synthesised from. */
|
|
70
75
|
const PolicyCheckToolShape = {
|
|
71
|
-
predicate
|
|
72
|
-
|
|
76
|
+
// Supply EITHER `transactionHash` OR both `predicate` and `permitTx`. The tree comes
|
|
77
|
+
// back from `synthesize_policy` only under `explain`, so a caller who did not
|
|
78
|
+
// ask for it has nothing to pass and would skip the check - and this IS the
|
|
79
|
+
// check. `transactionHash` lets the server rebuild both from the transaction.
|
|
80
|
+
predicate: run_1.PredicateNodeSchema.optional(),
|
|
81
|
+
// The canonical encoding `declare_policy` and `synthesize_policy` both
|
|
82
|
+
// return. One opaque string, where the tree is what callers mistype.
|
|
83
|
+
encodedPredicate: zod_1.z.string().optional(),
|
|
84
|
+
permitTx: run_1.RecordedTransactionSchema.optional(),
|
|
85
|
+
transactionHash: zod_1.z.string().optional(),
|
|
86
|
+
network: run_1.NetworkSchema.optional(),
|
|
87
|
+
// Needed with `transactionHash`: the predicate is lowered against the account it will
|
|
88
|
+
// be installed on.
|
|
89
|
+
smartAccount: zod_1.z.string().optional(),
|
|
90
|
+
userResponses: run_1.ComposeUserResponsesSchema.optional(),
|
|
73
91
|
validUntilLedger: zod_1.z.number().int().positive().optional(),
|
|
74
92
|
};
|
|
75
93
|
exports.SimulatePolicyToolShape = { ...PolicyCheckToolShape };
|
|
@@ -135,8 +153,46 @@ exports.InstallPolicyToolShape = {
|
|
|
135
153
|
* authority scan; omitting them returns `authorityScan: null`, which means
|
|
136
154
|
* "not checked" rather than "nothing found". */
|
|
137
155
|
existingRules: zod_1.z.array(zod_1.z.unknown()).optional(),
|
|
156
|
+
// Supply EITHER `rule` (the whole ContextRuleDraft) OR `fromHash`. Same
|
|
157
|
+
// reason as `synthesize_policy`: a draft nests i128 strings, a signer array
|
|
158
|
+
// and a policy array, and an agent retyping it out of its own transcript
|
|
159
|
+
// loses their types. `fromHash` re-derives the draft server-side from the
|
|
160
|
+
// recording, so the install runs against the rule the synthesizer produced.
|
|
138
161
|
rule: zod_1.z.unknown().optional(),
|
|
162
|
+
// Install a predicate you already hold - the base64 string `declare_policy`
|
|
163
|
+
// returns - naming the keys it governs as plain account addresses. Without
|
|
164
|
+
// this there is no route from `declare_policy` to here.
|
|
165
|
+
fromPredicate: zod_1.z
|
|
166
|
+
.object({
|
|
167
|
+
encodedPredicate: zod_1.z.string(),
|
|
168
|
+
signers: zod_1.z.array(zod_1.z.string()),
|
|
169
|
+
name: zod_1.z.string().optional(),
|
|
170
|
+
validUntilLedger: zod_1.z.number().int().positive().optional(),
|
|
171
|
+
})
|
|
172
|
+
.optional(),
|
|
173
|
+
fromHash: zod_1.z
|
|
174
|
+
.object({
|
|
175
|
+
transactionHash: zod_1.z.string(),
|
|
176
|
+
// The keys the rule governs, as plain Stellar addresses. Synthesis does
|
|
177
|
+
// not choose them, and a rule governing no key is refused on chain.
|
|
178
|
+
signers: zod_1.z.array(zod_1.z.string()).optional(),
|
|
179
|
+
userResponses: run_1.ComposeUserResponsesSchema.optional(),
|
|
180
|
+
})
|
|
181
|
+
.optional(),
|
|
139
182
|
installNonce: zod_1.z.number().int().positive().optional(),
|
|
183
|
+
// A ROLLING TOTAL beside the per-call predicate, as an OpenZeppelin
|
|
184
|
+
// spending_limit on the same rule. Both must permit. This is the only way to
|
|
185
|
+
// express "N per day": the interpreter is handed one call and keeps no state.
|
|
186
|
+
spendingLimit: zod_1.z
|
|
187
|
+
.object({
|
|
188
|
+
amount: zod_1.z.string(),
|
|
189
|
+
periodLedgers: zod_1.z.number().int().positive(),
|
|
190
|
+
})
|
|
191
|
+
.optional(),
|
|
192
|
+
// Opt-in to installing a rule that bounds no amount when the recording showed
|
|
193
|
+
// a spend. Default-deny: such a rule installs and verifies cleanly and caps
|
|
194
|
+
// nothing.
|
|
195
|
+
allowUnboundedAmount: zod_1.z.boolean().optional(),
|
|
140
196
|
interpreterAddress: zod_1.z.string().optional(),
|
|
141
197
|
};
|
|
142
198
|
/** Flat ZodRawShape for `revoke_policy`. */
|
package/dist-cjs/src/server.js
CHANGED
|
@@ -26,18 +26,22 @@ const toCallToolResult = (res) => (0, result_ts_1.mcpResultFromCore)(res);
|
|
|
26
26
|
/** Build a fresh, stateless MCP server. The caller owns the returned object
|
|
27
27
|
* and connects it to a single transport (stdio or Streamable HTTP). */
|
|
28
28
|
function createMcpServer() {
|
|
29
|
-
const server = new mcp_js_1.McpServer(
|
|
29
|
+
const server = new mcp_js_1.McpServer(
|
|
30
|
+
// Kept in step with package.json by `server-version.test.ts`. It was
|
|
31
|
+
// hardcoded once already and reported 0.0.0 to every client; a literal is
|
|
32
|
+
// fine, a literal nothing checks is not.
|
|
33
|
+
{ name: 'policy-builder-mcp', version: '1.1.0' }, { capabilities: { tools: {} } });
|
|
30
34
|
registerTools(server);
|
|
31
35
|
return server;
|
|
32
36
|
}
|
|
33
37
|
/** Idempotent registration of the T1 tool set on the given server. */
|
|
34
38
|
function registerTools(server) {
|
|
35
39
|
server.tool('record_transaction', 'Decode a Soroban transaction (on-chain hash OR base64 envelope XDR) into a RecordedTransaction. Returns a machine-readable ToolError on validation failure.', schemas_ts_1.RecordTransactionToolShape, (args) => (0, run_1.runRecordTransaction)(args).then(toCallToolResult));
|
|
36
|
-
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a
|
|
37
|
-
server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass the
|
|
38
|
-
server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose.', schemas_ts_1.VerifyPolicyToolShape, (args) =>
|
|
40
|
+
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a recorded transaction (`source: recording`). Pass `transactionHash` and `network` to have the server re-record and synthesize in one step - prefer this over copying a `recordedTx` payload back out of a previous tool result, which loses fields. `recordedTx` remains accepted for programmatic callers holding the object.', schemas_ts_1.SynthesizePolicyToolShape, (args) => (0, run_1.runSynthesizePolicy)(args).then(toCallToolResult));
|
|
41
|
+
server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass `transactionHash` with `smartAccount` and the server records and synthesizes the predicate itself; or pass a predicate together with the call to check it against. For a predicate you already hold, pass `encodedPredicate` (the base64 string `declare_policy` and `synthesize_policy` return) rather than retyping the `predicate` tree.', schemas_ts_1.SimulatePolicyToolShape, (args) => (0, run_1.runSimulatePolicy)(args).then(toCallToolResult));
|
|
42
|
+
server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose. Pass `transactionHash` with `smartAccount` and the server rebuilds the predicate and the recording itself; For a predicate you already hold - a DECLARED one has no recording behind it, so re-synthesizing would check something else - pass `encodedPredicate` (the base64 string) with `transactionHash`. Retyping the `predicate` tree is the error-prone path; do not take it, and never skip this check because the tree was not to hand.', schemas_ts_1.VerifyPolicyToolShape, (args) => (0, run_1.runVerifyPolicy)(args).then(toCallToolResult));
|
|
39
43
|
server.tool('declare_policy', "Build an interpreter predicate from a DECLARED constraint - the method to pin, and optionally the contract, a per-call amount cap, a recipient allowlist, and a minimum-output ratio (a swap slippage floor, bounding the output argument against the call's own input). Use this when there is no transaction to record, or when `record_transaction` refuses a contract it does not recognise. Returns the predicate tree, its canonical encoding and hash, ready for `install_policy`. `warnings` names any argument index that was GUESSED rather than supplied - a bound on the wrong argument constrains nothing while looking correct, so read them. There is no rolling spend window and no approval threshold: neither is expressible in grammar 4, and both belong to the OpenZeppelin account layer.", schemas_ts_1.DeclarePolicyToolShape, (args) => Promise.resolve((0, run_1.runDeclarePolicy)(args)).then(toCallToolResult));
|
|
40
|
-
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair).
|
|
44
|
+
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. Name the rule ONE of three ways. `fromPredicate: { encodedPredicate, signers }` installs a predicate you already hold - the base64 string `declare_policy` returns - and scopes the rule to whatever contract that predicate pins. `fromHash: { transactionHash, signers }` - with `fromHash` the server re-synthesizes the rule from the recording itself, and `signers` (plain G.../C... addresses) names the keys the rule governs. Naming them is required: synthesis reads a transaction and cannot decide which keys a rule binds, and a rule that governs no key is refused on chain. `rule` is the full ContextRuleDraft and is for programmatic callers only - retyping it loses the types of `validUntilLedger`, `signers` and `policies`. In BOTH handle forms `signers` is a list of plain Stellar account addresses (G...): a delegated signer is an account, NOT a deployed signer contract, and nothing needs to be deployed to name one. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair). `installNonce` defaults to 1, which is correct for a fresh rule - do not ask the caller for it. If the recorded call spends an amount and no `limitAmount` is given, the install is REFUSED: such a rule would constrain everything about the call except how much it moves, and it would install and verify cleanly while capping nothing. Supply `fromHash.userResponses.limitAmount`, or `allowUnboundedAmount: true` to do it deliberately. For a ROLLING TOTAL - "at most N per day" - pass `spendingLimit: { amount, periodLedgers }`. It attaches an OpenZeppelin `spending_limit` beside the predicate on the same rule, and both must permit. A predicate alone CANNOT express a rolling total: the interpreter is handed one call and keeps no state, so a per-call cap of N authorises N again on the very next call. The window counts LEDGERS (about five seconds each), and the rule must be scoped to the token contract whose transfers it meters. One transaction is all it takes: the smart account calls the interpreter `install` itself while running `add_context_rule`, so once this XDR is signed and submitted the rule is live and the predicate is enforced. The rule id the account assigned is in the transaction result. Do not tell the caller a second call is outstanding.', schemas_ts_1.InstallPolicyToolShape, (args) => (0, run_1.runInstallPolicy)(args).then(toCallToolResult));
|
|
41
45
|
server.tool('revoke_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.remove_context_rule(ruleId)` that removes a policy rule from the given smart account. The smart account handles uninstalling each attached policy itself. Auth is master-only - the source account MUST be the master signer set; delegated signers cannot uninstall.', schemas_ts_1.RevokePolicyToolShape, (args) => (0, run_1.runRevokePolicy)(args).then(toCallToolResult));
|
|
42
46
|
server.tool('get_interpreter_info', 'Read-only fingerprint lookup for the policy interpreter contract: returns the pinned address, grammar version, and wasm sha256 (from the pinned constants + SELF_VERSION). When `verifyLive=true`, performs an additional `grammar_version()` RPC call against the pinned address and reports whether the deployed contract matches the pin - a live mismatch check is more useful than a fabricated audit field.', schemas_ts_1.GetInterpreterInfoToolShape, (args) => (0, run_1.runGetInterpreterInfo)(args).then(toCallToolResult));
|
|
43
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-builder-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "MCP server exposing the OZ policy-synth core (record_transaction + synthesize_policy) over stdio and Streamable HTTP transports.",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"prepack": "bun run build"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@crediolabs/policy-synth": "
|
|
66
|
+
"@crediolabs/policy-synth": "1.1.0",
|
|
67
67
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
68
68
|
"@stellar/stellar-sdk": "14.4.0",
|
|
69
69
|
"zod": "3.25.76"
|
package/src/schemas.ts
CHANGED
|
@@ -70,7 +70,12 @@ export const RecordTransactionToolShape = {
|
|
|
70
70
|
* re-validates against the strict schema. */
|
|
71
71
|
export const SynthesizePolicyToolShape = {
|
|
72
72
|
source: z.literal('recording').optional(),
|
|
73
|
+
// Supply EITHER `recordedTx` (the whole recording) OR `transactionHash`. An agent
|
|
74
|
+
// should prefer the hash: it cannot copy a multi-thousand-character recording back out
|
|
75
|
+
// of its own transcript without losing fields, and the server can re-record
|
|
76
|
+
// from the hash deterministically.
|
|
73
77
|
recordedTx: RecordedTransactionSchema.optional(),
|
|
78
|
+
transactionHash: z.string().optional(),
|
|
74
79
|
network: NetworkSchema.optional(),
|
|
75
80
|
userResponses: ComposeUserResponsesSchema.optional(),
|
|
76
81
|
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
@@ -89,8 +94,21 @@ export const SynthesizePolicyToolShape = {
|
|
|
89
94
|
* (from `synthesize_policy` under `explain`) plus the recording it was
|
|
90
95
|
* synthesised from. */
|
|
91
96
|
const PolicyCheckToolShape = {
|
|
92
|
-
predicate
|
|
93
|
-
|
|
97
|
+
// Supply EITHER `transactionHash` OR both `predicate` and `permitTx`. The tree comes
|
|
98
|
+
// back from `synthesize_policy` only under `explain`, so a caller who did not
|
|
99
|
+
// ask for it has nothing to pass and would skip the check - and this IS the
|
|
100
|
+
// check. `transactionHash` lets the server rebuild both from the transaction.
|
|
101
|
+
predicate: PredicateNodeSchema.optional(),
|
|
102
|
+
// The canonical encoding `declare_policy` and `synthesize_policy` both
|
|
103
|
+
// return. One opaque string, where the tree is what callers mistype.
|
|
104
|
+
encodedPredicate: z.string().optional(),
|
|
105
|
+
permitTx: RecordedTransactionSchema.optional(),
|
|
106
|
+
transactionHash: z.string().optional(),
|
|
107
|
+
network: NetworkSchema.optional(),
|
|
108
|
+
// Needed with `transactionHash`: the predicate is lowered against the account it will
|
|
109
|
+
// be installed on.
|
|
110
|
+
smartAccount: z.string().optional(),
|
|
111
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
94
112
|
validUntilLedger: z.number().int().positive().optional(),
|
|
95
113
|
} as const
|
|
96
114
|
|
|
@@ -168,8 +186,46 @@ export const InstallPolicyToolShape = {
|
|
|
168
186
|
* authority scan; omitting them returns `authorityScan: null`, which means
|
|
169
187
|
* "not checked" rather than "nothing found". */
|
|
170
188
|
existingRules: z.array(z.unknown()).optional(),
|
|
189
|
+
// Supply EITHER `rule` (the whole ContextRuleDraft) OR `fromHash`. Same
|
|
190
|
+
// reason as `synthesize_policy`: a draft nests i128 strings, a signer array
|
|
191
|
+
// and a policy array, and an agent retyping it out of its own transcript
|
|
192
|
+
// loses their types. `fromHash` re-derives the draft server-side from the
|
|
193
|
+
// recording, so the install runs against the rule the synthesizer produced.
|
|
171
194
|
rule: z.unknown().optional(),
|
|
195
|
+
// Install a predicate you already hold - the base64 string `declare_policy`
|
|
196
|
+
// returns - naming the keys it governs as plain account addresses. Without
|
|
197
|
+
// this there is no route from `declare_policy` to here.
|
|
198
|
+
fromPredicate: z
|
|
199
|
+
.object({
|
|
200
|
+
encodedPredicate: z.string(),
|
|
201
|
+
signers: z.array(z.string()),
|
|
202
|
+
name: z.string().optional(),
|
|
203
|
+
validUntilLedger: z.number().int().positive().optional(),
|
|
204
|
+
})
|
|
205
|
+
.optional(),
|
|
206
|
+
fromHash: z
|
|
207
|
+
.object({
|
|
208
|
+
transactionHash: z.string(),
|
|
209
|
+
// The keys the rule governs, as plain Stellar addresses. Synthesis does
|
|
210
|
+
// not choose them, and a rule governing no key is refused on chain.
|
|
211
|
+
signers: z.array(z.string()).optional(),
|
|
212
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
213
|
+
})
|
|
214
|
+
.optional(),
|
|
172
215
|
installNonce: z.number().int().positive().optional(),
|
|
216
|
+
// A ROLLING TOTAL beside the per-call predicate, as an OpenZeppelin
|
|
217
|
+
// spending_limit on the same rule. Both must permit. This is the only way to
|
|
218
|
+
// express "N per day": the interpreter is handed one call and keeps no state.
|
|
219
|
+
spendingLimit: z
|
|
220
|
+
.object({
|
|
221
|
+
amount: z.string(),
|
|
222
|
+
periodLedgers: z.number().int().positive(),
|
|
223
|
+
})
|
|
224
|
+
.optional(),
|
|
225
|
+
// Opt-in to installing a rule that bounds no amount when the recording showed
|
|
226
|
+
// a spend. Default-deny: such a rule installs and verifies cleanly and caps
|
|
227
|
+
// nothing.
|
|
228
|
+
allowUnboundedAmount: z.boolean().optional(),
|
|
173
229
|
interpreterAddress: z.string().optional(),
|
|
174
230
|
} as const
|
|
175
231
|
|
package/src/server.ts
CHANGED
|
@@ -48,7 +48,10 @@ const toCallToolResult = <T>(
|
|
|
48
48
|
* and connects it to a single transport (stdio or Streamable HTTP). */
|
|
49
49
|
export function createMcpServer(): McpServer {
|
|
50
50
|
const server = new McpServer(
|
|
51
|
-
|
|
51
|
+
// Kept in step with package.json by `server-version.test.ts`. It was
|
|
52
|
+
// hardcoded once already and reported 0.0.0 to every client; a literal is
|
|
53
|
+
// fine, a literal nothing checks is not.
|
|
54
|
+
{ name: 'policy-builder-mcp', version: '1.1.0' },
|
|
52
55
|
{ capabilities: { tools: {} } }
|
|
53
56
|
)
|
|
54
57
|
registerTools(server)
|
|
@@ -66,23 +69,23 @@ export function registerTools(server: McpServer): void {
|
|
|
66
69
|
|
|
67
70
|
server.tool(
|
|
68
71
|
'synthesize_policy',
|
|
69
|
-
'Synthesize a ProposedPolicy from a
|
|
72
|
+
'Synthesize a ProposedPolicy from a recorded transaction (`source: recording`). Pass `transactionHash` and `network` to have the server re-record and synthesize in one step - prefer this over copying a `recordedTx` payload back out of a previous tool result, which loses fields. `recordedTx` remains accepted for programmatic callers holding the object.',
|
|
70
73
|
SynthesizePolicyToolShape,
|
|
71
74
|
(args) => runSynthesizePolicy(args).then(toCallToolResult)
|
|
72
75
|
)
|
|
73
76
|
|
|
74
77
|
server.tool(
|
|
75
78
|
'simulate_policy',
|
|
76
|
-
'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass the
|
|
79
|
+
'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass `transactionHash` with `smartAccount` and the server records and synthesizes the predicate itself; or pass a predicate together with the call to check it against. For a predicate you already hold, pass `encodedPredicate` (the base64 string `declare_policy` and `synthesize_policy` return) rather than retyping the `predicate` tree.',
|
|
77
80
|
SimulatePolicyToolShape,
|
|
78
|
-
(args) =>
|
|
81
|
+
(args) => runSimulatePolicy(args).then(toCallToolResult)
|
|
79
82
|
)
|
|
80
83
|
|
|
81
84
|
server.tool(
|
|
82
85
|
'verify_policy',
|
|
83
|
-
'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose.',
|
|
86
|
+
'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose. Pass `transactionHash` with `smartAccount` and the server rebuilds the predicate and the recording itself; For a predicate you already hold - a DECLARED one has no recording behind it, so re-synthesizing would check something else - pass `encodedPredicate` (the base64 string) with `transactionHash`. Retyping the `predicate` tree is the error-prone path; do not take it, and never skip this check because the tree was not to hand.',
|
|
84
87
|
VerifyPolicyToolShape,
|
|
85
|
-
(args) =>
|
|
88
|
+
(args) => runVerifyPolicy(args).then(toCallToolResult)
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
server.tool(
|
|
@@ -94,7 +97,7 @@ export function registerTools(server: McpServer): void {
|
|
|
94
97
|
|
|
95
98
|
server.tool(
|
|
96
99
|
'install_policy',
|
|
97
|
-
'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair).
|
|
100
|
+
'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. Name the rule ONE of three ways. `fromPredicate: { encodedPredicate, signers }` installs a predicate you already hold - the base64 string `declare_policy` returns - and scopes the rule to whatever contract that predicate pins. `fromHash: { transactionHash, signers }` - with `fromHash` the server re-synthesizes the rule from the recording itself, and `signers` (plain G.../C... addresses) names the keys the rule governs. Naming them is required: synthesis reads a transaction and cannot decide which keys a rule binds, and a rule that governs no key is refused on chain. `rule` is the full ContextRuleDraft and is for programmatic callers only - retyping it loses the types of `validUntilLedger`, `signers` and `policies`. In BOTH handle forms `signers` is a list of plain Stellar account addresses (G...): a delegated signer is an account, NOT a deployed signer contract, and nothing needs to be deployed to name one. The response carries an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair). `installNonce` defaults to 1, which is correct for a fresh rule - do not ask the caller for it. If the recorded call spends an amount and no `limitAmount` is given, the install is REFUSED: such a rule would constrain everything about the call except how much it moves, and it would install and verify cleanly while capping nothing. Supply `fromHash.userResponses.limitAmount`, or `allowUnboundedAmount: true` to do it deliberately. For a ROLLING TOTAL - "at most N per day" - pass `spendingLimit: { amount, periodLedgers }`. It attaches an OpenZeppelin `spending_limit` beside the predicate on the same rule, and both must permit. A predicate alone CANNOT express a rolling total: the interpreter is handed one call and keeps no state, so a per-call cap of N authorises N again on the very next call. The window counts LEDGERS (about five seconds each), and the rule must be scoped to the token contract whose transfers it meters. One transaction is all it takes: the smart account calls the interpreter `install` itself while running `add_context_rule`, so once this XDR is signed and submitted the rule is live and the predicate is enforced. The rule id the account assigned is in the transaction result. Do not tell the caller a second call is outstanding.',
|
|
98
101
|
InstallPolicyToolShape,
|
|
99
102
|
(args) => runInstallPolicy(args).then(toCallToolResult)
|
|
100
103
|
)
|