@ratifia/mcp 0.10.1 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +29 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,8 +57,8 @@ real decisions. CI runs it on every push.
57
57
  | `RATIFIA_API_KEY` | ✅ | — | Org-scoped worker API key. |
58
58
  | `RATIFIA_API_URL` | | `https://api.ratifia.com` | Ratifia API base. |
59
59
  | `RATIFIA_APP_URL` | | `https://app.ratifia.com` | Dashboard base for decision links. |
60
- | `RATIFIA_APPROVER_EMAIL` | | — | Who the decision is assigned to / notified. |
61
- | `RATIFIA_POLICY` | | — | The **key** of a policy deciding **who** reviews and the quorum they need. It must exist in your org: an unknown key is a tool error, and the `gate` hook denies. |
60
+ | `RATIFIA_APPROVER_EMAIL` | | — | Without a policy: who can answer, one email or a comma-separated list. Ignored when `RATIFIA_POLICY` is set. |
61
+ | `RATIFIA_POLICY` | | — | The **key** of a policy deciding **who** reviews, the quorum they need, and the expiry (`timeout_minutes` is then not sent). It must exist in your org: an unknown key is a tool error, and the `gate` hook denies. |
62
62
  | `RATIFIA_GATE_INCLUDE_PROMPT` | | off | `gate` hook only: `1` includes the user's last request (from the transcript) in the decision. Off by default — the gate otherwise sends only metadata, never your conversation. |
63
63
  | `RATIFIA_WORKFLOW` | | `ratifia-mcp` (`ratifia-gate` for the hook) | The **workflow** decisions are filed under — the category the dashboard's Workflow filter groups by, e.g. `deploys` or `billing-agent`. A `request_decision` call can override it. |
64
64
 
@@ -205,7 +205,7 @@ Three properties worth knowing, because they shape how you should use it:
205
205
 
206
206
  1. The agent calls `request_decision` before a consequential/irreversible step (deploy, delete, spend, send externally), an ambiguous call it shouldn't guess, or to get a human to pick/refine something.
207
207
  2. Ratifia notifies the human (even away from keyboard) per the org's routing.
208
- 3. The agent `await_decision`s (or polls `check_decision`) — read `verdict`/`note`, plus `response_value` (select/text) or `output` (a refined draft).
208
+ 3. The agent `await_decision`s (or polls `check_decision`) — read `verdict`/`note`, plus `response_value` (select/text) or `output` (a refined draft). `policy_version` / `policy_version_id` say which version of the policy the decision ran under (null when no policy applied).
209
209
  4. The agent proceeds only on `approved`; on `rejected`/`expired` it adapts.
210
210
 
211
211
  Learn more at [ratifia.com](https://ratifia.com).
package/dist/index.js CHANGED
@@ -24,8 +24,11 @@
24
24
  * RATIFIA_API_URL API base (default https://api.ratifia.com)
25
25
  * RATIFIA_API_KEY org-scoped worker API key (required)
26
26
  * RATIFIA_APP_URL dashboard base for the decision link (default https://app.ratifia.com)
27
- * RATIFIA_APPROVER_EMAIL who the decision is assigned to / notified
28
- * RATIFIA_POLICY optional: policy key deciding who reviews (must exist in the org)
27
+ * RATIFIA_APPROVER_EMAIL who may answer (comma-separated for several); used only
28
+ * when RATIFIA_POLICY is unset
29
+ * RATIFIA_POLICY optional: policy key (must exist in the org). When set, the
30
+ * policy decides who approves and when it expires, and
31
+ * RATIFIA_APPROVER_EMAIL / timeout_minutes are not sent
29
32
  * (e.g. a route to your phone)
30
33
  * RATIFIA_GATE_INCLUDE_PROMPT gate hook only: 1 = include the user's last request from the
31
34
  * transcript in the decision (off by default — it is case content)
@@ -42,8 +45,21 @@ import { z } from 'zod';
42
45
  const API_URL = (process.env.RATIFIA_API_URL ?? 'https://api.ratifia.com').replace(/\/+$/, '');
43
46
  const API_KEY = process.env.RATIFIA_API_KEY;
44
47
  const APP_URL = (process.env.RATIFIA_APP_URL ?? 'https://app.ratifia.com').replace(/\/+$/, '');
45
- const APPROVER_EMAIL = process.env.RATIFIA_APPROVER_EMAIL;
46
- const POLICY = process.env.RATIFIA_POLICY;
48
+ const APPROVERS = (process.env.RATIFIA_APPROVER_EMAIL ?? '')
49
+ .split(',')
50
+ .map((s) => s.trim())
51
+ .filter(Boolean);
52
+ const POLICY = process.env.RATIFIA_POLICY || undefined;
53
+ /**
54
+ * Who answers (Ratifia PD-003): a policy owns its approvers and expiry, so with
55
+ * one set only its key is sent (approvers alongside it are refused, 422
56
+ * field_owned_by_policy). Without one, the decision is ad hoc to APPROVERS.
57
+ */
58
+ const WHO = POLICY
59
+ ? { policy: { key: POLICY } }
60
+ : APPROVERS.length
61
+ ? { approvers: APPROVERS }
62
+ : {};
47
63
  const WORKFLOW = process.env.RATIFIA_WORKFLOW;
48
64
  /**
49
65
  * Where a decision is filed. `workflowRef` is the category (the dashboard's
@@ -188,6 +204,10 @@ const formatDecision = (d) => ({
188
204
  ...(d.output ? { output: d.output, refined: Boolean(d.refinedOutput) } : {}),
189
205
  decided_by: d.decidedBy,
190
206
  decided_at: d.decidedAt,
207
+ // FN-316: which version of the policy governed this decision, so an agent can
208
+ // cite it ("approved under refunds v3"). Stays put when the policy is edited.
209
+ policy_version: d.policyVersion ?? null,
210
+ policy_version_id: d.policyVersionId ?? null,
191
211
  });
192
212
  const asToolResult = (payload) => ({
193
213
  content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
@@ -406,8 +426,7 @@ async function runGate() {
406
426
  externalRunId,
407
427
  stepRef: `tool:${toolName}`.slice(0, 80),
408
428
  workflowRef: label(WORKFLOW, 'ratifia-gate', 100),
409
- ...(APPROVER_EMAIL ? { assigneeEmail: APPROVER_EMAIL } : {}),
410
- ...(POLICY ? { policy: POLICY } : {}),
429
+ ...WHO,
411
430
  required: true,
412
431
  context: {
413
432
  prompt: `An agent wants to run the tool "${toolName}"${where ? ` in ${where}` : ''}. Approve?`,
@@ -489,7 +508,7 @@ server.registerTool('request_decision', {
489
508
  .int()
490
509
  .positive()
491
510
  .optional()
492
- .describe('Auto-expire (and reject) if unanswered after N minutes. Omit to wait indefinitely.'),
511
+ .describe("Auto-expire (and reject) if unanswered after N minutes. Omit to wait indefinitely. Ignored when this server is configured with a policy (RATIFIA_POLICY): the policy's expiry applies."),
493
512
  response_type: z
494
513
  .enum(['approval', 'select', 'text'])
495
514
  .optional()
@@ -571,10 +590,10 @@ server.registerTool('request_decision', {
571
590
  externalRunId,
572
591
  stepRef: label(step, title, 80),
573
592
  workflowRef: label(workflow, label(WORKFLOW, 'ratifia-mcp', 100), 100),
574
- ...(APPROVER_EMAIL ? { assigneeEmail: APPROVER_EMAIL } : {}),
575
- ...(POLICY ? { policy: POLICY } : {}),
593
+ ...WHO,
576
594
  required: true,
577
- ...(timeout_minutes ? { timeoutSec: timeout_minutes * 60 } : {}),
595
+ // With a policy, the policy sets the expiry (a timeoutSec would be refused).
596
+ ...(timeout_minutes && !POLICY ? { timeoutSec: timeout_minutes * 60 } : {}),
578
597
  ...(responseSpec ? { responseSpec } : {}),
579
598
  ...(proposed_output
580
599
  ? { proposedOutput: { title: proposed_output.title, format: 'text', content: proposed_output.content } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ratifia/mcp",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Ratifia MCP server — let an AI agent request a human decision/approval (HITL) mid-task and resume once it's answered. The agent becomes a Ratifia 'engine'.",
6
6
  "keywords": [