@paystage/policy 0.2.0 → 0.2.1
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/README.md +11 -0
- package/dist/agents.d.ts +66 -0
- package/dist/agents.js +106 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schema.d.ts +40 -0
- package/dist/schema.js +16 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,19 @@ humanToAtomic("0.50"); // "500000"
|
|
|
40
40
|
- `loadPolicy(path)` — read + parse a JSON policy file.
|
|
41
41
|
- `humanToAtomic(amount)` — convert `"0.50"` → `"500000"` (USDC, 6 decimals).
|
|
42
42
|
|
|
43
|
+
## Supply chain and access
|
|
44
|
+
|
|
45
|
+
| Flag | Why it is there |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Filesystem access | `loadPolicy()` reads one JSON file — the policy it evaluates against. It never writes. |
|
|
48
|
+
| Environment variable access | `PAYSTAGE_POLICY_PATH`, as a fallback when no path argument is given. |
|
|
49
|
+
| Network access | None. This package is pure: policy in, normalised policy out. |
|
|
50
|
+
|
|
51
|
+
No shell, no `eval`, no telemetry. Full table: [SECURITY.md](../../SECURITY.md).
|
|
52
|
+
|
|
43
53
|
## Links
|
|
44
54
|
|
|
45
55
|
- Website: <https://paystage.xyz>
|
|
56
|
+
- X: <https://x.com/paystage_xyz>
|
|
46
57
|
- Source: <https://github.com/base-buzz/paystage>
|
|
47
58
|
- License: MIT
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent budgets.
|
|
3
|
+
*
|
|
4
|
+
* A single policy for every agent on a machine is the wrong shape: the reading bot
|
|
5
|
+
* that summarises articles and the trading bot that moves size should not share a
|
|
6
|
+
* ceiling. Matching a payer's leash (which is global and operator-set) is not enough;
|
|
7
|
+
* the rules need to be per-identity and they need to live with the operator.
|
|
8
|
+
*
|
|
9
|
+
* Two properties make this safe to add to an existing policy:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Agents can only narrow, never widen.** An override that raises a ceiling, adds
|
|
12
|
+
* a payee or loosens `unknown_payee` is rejected at load, not silently applied.
|
|
13
|
+
* Otherwise a typo in one block becomes a spend incident.
|
|
14
|
+
* 2. **Fail closed on identity.** Once a policy declares agents, a request without a
|
|
15
|
+
* known agent id is denied. Absent identity must never fall back to the global
|
|
16
|
+
* policy, because the global policy is the loosest thing in the file.
|
|
17
|
+
*/
|
|
18
|
+
import type { AmountSpec, Asset, Policy, UnknownPayee, VelocityWindow } from "./types.js";
|
|
19
|
+
export interface AgentBudget {
|
|
20
|
+
/** Per-call ceiling. Must be <= the policy's `max_amount`. */
|
|
21
|
+
max_amount?: AmountSpec;
|
|
22
|
+
/** Per-day ceiling for this agent. Must be <= the policy's `daily_cap`. */
|
|
23
|
+
daily_cap?: AmountSpec;
|
|
24
|
+
/** Narrower payee list. Every entry must already be in `allowed_pay_to`. */
|
|
25
|
+
allowed_pay_to?: string[];
|
|
26
|
+
/** Narrower asset list. Every entry must already be in `allowed_assets`. */
|
|
27
|
+
allowed_assets?: Asset[];
|
|
28
|
+
/** Narrower network list. */
|
|
29
|
+
allowed_networks?: string[];
|
|
30
|
+
/** `review` may be tightened to `deny`; the reverse is rejected. */
|
|
31
|
+
unknown_payee?: UnknownPayee;
|
|
32
|
+
/** Overrides `require_human_above`, and must be lower (or equal). */
|
|
33
|
+
require_human_above?: AmountSpec;
|
|
34
|
+
/** Overrides the policy's velocity windows. */
|
|
35
|
+
velocity?: {
|
|
36
|
+
windows: VelocityWindow[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface AgentPolicy extends Policy {
|
|
40
|
+
/** The id used to resolve this policy. */
|
|
41
|
+
agent_id: string;
|
|
42
|
+
/** Kept for completeness: the agent block is a narrowing overlay. */
|
|
43
|
+
agents: NonNullable<Policy["agents"]>;
|
|
44
|
+
}
|
|
45
|
+
/** Compare two amount specs with the same asset and decimals. */
|
|
46
|
+
export declare function compareAmounts(a: AmountSpec, b: AmountSpec): number;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a policy for one agent, or explain why it cannot be resolved.
|
|
49
|
+
*
|
|
50
|
+
* Returns a discriminated result rather than throwing, because the caller is a
|
|
51
|
+
* payment path: a missing agent must produce `do_not_pay`, not an exception.
|
|
52
|
+
*/
|
|
53
|
+
export type AgentResolution = {
|
|
54
|
+
ok: true;
|
|
55
|
+
policy: Policy;
|
|
56
|
+
agentId: string;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function resolveAgentPolicy(policy: Policy, agentId: string | undefined): AgentResolution;
|
|
62
|
+
/**
|
|
63
|
+
* Reject any override that loosens the global policy. Returns a human-readable
|
|
64
|
+
* problem or `undefined` when the block only narrows.
|
|
65
|
+
*/
|
|
66
|
+
export declare function findWidening(policy: Policy, budget: AgentBudget): string | undefined;
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent budgets.
|
|
3
|
+
*
|
|
4
|
+
* A single policy for every agent on a machine is the wrong shape: the reading bot
|
|
5
|
+
* that summarises articles and the trading bot that moves size should not share a
|
|
6
|
+
* ceiling. Matching a payer's leash (which is global and operator-set) is not enough;
|
|
7
|
+
* the rules need to be per-identity and they need to live with the operator.
|
|
8
|
+
*
|
|
9
|
+
* Two properties make this safe to add to an existing policy:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Agents can only narrow, never widen.** An override that raises a ceiling, adds
|
|
12
|
+
* a payee or loosens `unknown_payee` is rejected at load, not silently applied.
|
|
13
|
+
* Otherwise a typo in one block becomes a spend incident.
|
|
14
|
+
* 2. **Fail closed on identity.** Once a policy declares agents, a request without a
|
|
15
|
+
* known agent id is denied. Absent identity must never fall back to the global
|
|
16
|
+
* policy, because the global policy is the loosest thing in the file.
|
|
17
|
+
*/
|
|
18
|
+
function toBaseUnits(spec) {
|
|
19
|
+
const [whole, fraction = ""] = spec.value.split(".");
|
|
20
|
+
const padded = (fraction + "0".repeat(spec.decimals)).slice(0, spec.decimals);
|
|
21
|
+
return BigInt(`${whole}${padded}` || "0");
|
|
22
|
+
}
|
|
23
|
+
/** Compare two amount specs with the same asset and decimals. */
|
|
24
|
+
export function compareAmounts(a, b) {
|
|
25
|
+
const ba = toBaseUnits(a);
|
|
26
|
+
const bb = toBaseUnits(b);
|
|
27
|
+
return ba < bb ? -1 : ba > bb ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
30
|
+
export function resolveAgentPolicy(policy, agentId) {
|
|
31
|
+
const agents = policy.agents;
|
|
32
|
+
if (agents === undefined || Object.keys(agents).length === 0) {
|
|
33
|
+
// No per-agent blocks: the global policy applies, and identity is optional.
|
|
34
|
+
return { ok: true, policy, agentId: agentId ?? "default" };
|
|
35
|
+
}
|
|
36
|
+
if (agentId === undefined || agentId.trim() === "") {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "this policy defines per-agent budgets, so the caller must identify itself with agent_id",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const budget = agents[agentId];
|
|
43
|
+
if (budget === undefined) {
|
|
44
|
+
return { ok: false, reason: `unknown agent "${agentId}" — not listed in policy.agents` };
|
|
45
|
+
}
|
|
46
|
+
const merged = {
|
|
47
|
+
...policy,
|
|
48
|
+
max_amount: budget.max_amount ?? policy.max_amount,
|
|
49
|
+
daily_cap: budget.daily_cap ?? policy.daily_cap,
|
|
50
|
+
allowed_pay_to: budget.allowed_pay_to ?? policy.allowed_pay_to,
|
|
51
|
+
allowed_assets: budget.allowed_assets ?? policy.allowed_assets,
|
|
52
|
+
allowed_networks: budget.allowed_networks ?? policy.allowed_networks,
|
|
53
|
+
unknown_payee: budget.unknown_payee ?? policy.unknown_payee,
|
|
54
|
+
velocity: budget.velocity ?? policy.velocity,
|
|
55
|
+
...(budget.require_human_above === undefined && policy.require_human_above === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { require_human_above: budget.require_human_above ?? policy.require_human_above }),
|
|
58
|
+
};
|
|
59
|
+
const violation = findWidening(policy, budget);
|
|
60
|
+
if (violation !== undefined)
|
|
61
|
+
return { ok: false, reason: violation };
|
|
62
|
+
return { ok: true, policy: merged, agentId };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reject any override that loosens the global policy. Returns a human-readable
|
|
66
|
+
* problem or `undefined` when the block only narrows.
|
|
67
|
+
*/
|
|
68
|
+
export function findWidening(policy, budget) {
|
|
69
|
+
if (budget.max_amount !== undefined && compareAmounts(budget.max_amount, policy.max_amount) > 0) {
|
|
70
|
+
return "agent max_amount is above the policy max_amount — overrides may only narrow";
|
|
71
|
+
}
|
|
72
|
+
if (budget.daily_cap !== undefined &&
|
|
73
|
+
policy.daily_cap !== undefined &&
|
|
74
|
+
compareAmounts(budget.daily_cap, policy.daily_cap) > 0) {
|
|
75
|
+
return "agent daily_cap is above the policy daily_cap — overrides may only narrow";
|
|
76
|
+
}
|
|
77
|
+
if (budget.allowed_pay_to !== undefined) {
|
|
78
|
+
const outside = budget.allowed_pay_to.filter((entry) => !policy.allowed_pay_to.some((allowed) => sameAddress(allowed, entry)));
|
|
79
|
+
if (outside.length > 0) {
|
|
80
|
+
return `agent allowed_pay_to adds payees outside the policy list: ${outside.join(", ")}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (budget.allowed_networks !== undefined) {
|
|
84
|
+
const outside = budget.allowed_networks.filter((n) => !policy.allowed_networks.includes(n));
|
|
85
|
+
if (outside.length > 0) {
|
|
86
|
+
return `agent allowed_networks adds networks outside the policy list: ${outside.join(", ")}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (budget.allowed_assets !== undefined) {
|
|
90
|
+
const outside = budget.allowed_assets.filter((asset) => !policy.allowed_assets.some((allowed) => sameAddress(allowed.address, asset.address) && allowed.network === asset.network));
|
|
91
|
+
if (outside.length > 0) {
|
|
92
|
+
return `agent allowed_assets adds assets outside the policy list: ${outside
|
|
93
|
+
.map((a) => a.symbol)
|
|
94
|
+
.join(", ")}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (budget.unknown_payee === "review" && policy.unknown_payee === "deny") {
|
|
98
|
+
return "agent unknown_payee loosens deny to review";
|
|
99
|
+
}
|
|
100
|
+
if (budget.require_human_above !== undefined &&
|
|
101
|
+
policy.require_human_above !== undefined &&
|
|
102
|
+
compareAmounts(budget.require_human_above, policy.require_human_above) > 0) {
|
|
103
|
+
return "agent require_human_above is above the policy threshold — overrides may only narrow";
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -44,6 +44,46 @@ export declare const PolicySchema: z.ZodObject<{
|
|
|
44
44
|
}, z.core.$strip>>;
|
|
45
45
|
}, z.core.$strip>>;
|
|
46
46
|
}, z.core.$strip>>;
|
|
47
|
+
agents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
48
|
+
max_amount: z.ZodOptional<z.ZodObject<{
|
|
49
|
+
value: z.ZodString;
|
|
50
|
+
asset: z.ZodString;
|
|
51
|
+
decimals: z.ZodNumber;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
daily_cap: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
value: z.ZodString;
|
|
55
|
+
asset: z.ZodString;
|
|
56
|
+
decimals: z.ZodNumber;
|
|
57
|
+
}, z.core.$strip>>;
|
|
58
|
+
allowed_pay_to: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
|
+
allowed_assets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
60
|
+
symbol: z.ZodString;
|
|
61
|
+
decimals: z.ZodNumber;
|
|
62
|
+
address: z.ZodString;
|
|
63
|
+
network: z.ZodString;
|
|
64
|
+
}, z.core.$strip>>>;
|
|
65
|
+
allowed_networks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
66
|
+
unknown_payee: z.ZodOptional<z.ZodEnum<{
|
|
67
|
+
review: "review";
|
|
68
|
+
deny: "deny";
|
|
69
|
+
}>>;
|
|
70
|
+
require_human_above: z.ZodOptional<z.ZodObject<{
|
|
71
|
+
value: z.ZodString;
|
|
72
|
+
asset: z.ZodString;
|
|
73
|
+
decimals: z.ZodNumber;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
velocity: z.ZodOptional<z.ZodObject<{
|
|
76
|
+
windows: z.ZodArray<z.ZodObject<{
|
|
77
|
+
window_seconds: z.ZodNumber;
|
|
78
|
+
max_count: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
max_amount: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
value: z.ZodString;
|
|
81
|
+
decimals: z.ZodNumber;
|
|
82
|
+
asset: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
}, z.core.$strip>>;
|
|
86
|
+
}, z.core.$strip>>>;
|
|
47
87
|
}, z.core.$strip>;
|
|
48
88
|
export type PolicyFromSchema = z.infer<typeof PolicySchema>;
|
|
49
89
|
export type ParsedPolicy = Policy;
|
package/dist/schema.js
CHANGED
|
@@ -46,5 +46,21 @@ export const PolicySchema = z.object({
|
|
|
46
46
|
disallow_unlimited_approvals: z.boolean().optional(),
|
|
47
47
|
velocity: z
|
|
48
48
|
.object({ windows: z.array(velocityWindowSchema).min(1) })
|
|
49
|
+
.optional(),
|
|
50
|
+
/**
|
|
51
|
+
* Per-agent overlays. Shape is validated here; the *narrowing* rule is enforced in
|
|
52
|
+
* `resolveAgentPolicy`, because it needs the surrounding policy to compare against.
|
|
53
|
+
*/
|
|
54
|
+
agents: z
|
|
55
|
+
.record(z.string().min(1), z.object({
|
|
56
|
+
max_amount: amountSpecSchema.optional(),
|
|
57
|
+
daily_cap: amountSpecSchema.optional(),
|
|
58
|
+
allowed_pay_to: z.array(addressSchema).optional(),
|
|
59
|
+
allowed_assets: z.array(assetSchema).optional(),
|
|
60
|
+
allowed_networks: z.array(z.string().regex(/^eip155:\d+$/)).optional(),
|
|
61
|
+
unknown_payee: z.enum(["review", "deny"]).optional(),
|
|
62
|
+
require_human_above: amountSpecSchema.optional(),
|
|
63
|
+
velocity: z.object({ windows: z.array(velocityWindowSchema).min(1) }).optional(),
|
|
64
|
+
}))
|
|
49
65
|
.optional()
|
|
50
66
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -40,4 +40,9 @@ export interface Policy {
|
|
|
40
40
|
velocity?: {
|
|
41
41
|
windows: VelocityWindow[];
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Per-agent narrowing overlays. When present, a caller must pass a known
|
|
45
|
+
* `agent_id`; an override may only tighten the fields above, never loosen them.
|
|
46
|
+
*/
|
|
47
|
+
agents?: Record<string, import("./agents.js").AgentBudget>;
|
|
43
48
|
}
|