@nestpilot/mcp-app 1.0.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 (149) hide show
  1. package/README.md +350 -0
  2. package/dist/cli/doctor.d.ts +1 -0
  3. package/dist/cli/doctor.js +214 -0
  4. package/dist/cli/export-import.d.ts +6 -0
  5. package/dist/cli/export-import.js +132 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +168 -0
  8. package/dist/cli/init.d.ts +1 -0
  9. package/dist/cli/init.js +171 -0
  10. package/dist/host-configs/cowork.json +11 -0
  11. package/dist/host-configs/goose.yaml +22 -0
  12. package/dist/host-configs/openclaw-manifest.json +16 -0
  13. package/dist/main.d.ts +2 -0
  14. package/dist/main.js +128 -0
  15. package/dist/mcp-app.html +155 -0
  16. package/dist/nestpilot-client.d.ts +44 -0
  17. package/dist/nestpilot-client.js +160 -0
  18. package/dist/planner.html +222 -0
  19. package/dist/server.d.ts +19 -0
  20. package/dist/server.js +245 -0
  21. package/dist/skills/SKILL.md +162 -0
  22. package/dist/skills/manifest.json +51 -0
  23. package/dist/skills/tools/activate_plan.md +36 -0
  24. package/dist/skills/tools/coach.md +59 -0
  25. package/dist/skills/tools/comprehensive_plan.md +65 -0
  26. package/dist/skills/tools/create_plan.md +59 -0
  27. package/dist/skills/tools/create_saved_plan.md +49 -0
  28. package/dist/skills/tools/delete_plan.md +42 -0
  29. package/dist/skills/tools/delete_scenario.md +38 -0
  30. package/dist/skills/tools/generate_proposal.md +63 -0
  31. package/dist/skills/tools/generate_retirement_report.md +50 -0
  32. package/dist/skills/tools/get_active_plan.md +44 -0
  33. package/dist/skills/tools/get_baseline_forecast.md +47 -0
  34. package/dist/skills/tools/get_plan.md +44 -0
  35. package/dist/skills/tools/get_plan_components.md +50 -0
  36. package/dist/skills/tools/get_scenario.md +46 -0
  37. package/dist/skills/tools/list_plans.md +44 -0
  38. package/dist/skills/tools/list_scenarios.md +42 -0
  39. package/dist/skills/tools/medicare-guardian.md +59 -0
  40. package/dist/skills/tools/nestpilot_run_plan.md +61 -0
  41. package/dist/skills/tools/optimize_roth_conversion.md +107 -0
  42. package/dist/skills/tools/optimize_ss_claiming.md +30 -0
  43. package/dist/skills/tools/rename_plan.md +34 -0
  44. package/dist/skills/tools/retirement-planner.md +55 -0
  45. package/dist/skills/tools/run_forecast.md +65 -0
  46. package/dist/skills/tools/run_saved_forecast.md +52 -0
  47. package/dist/skills/tools/run_scenario.md +66 -0
  48. package/dist/skills/tools/save_plan.md +48 -0
  49. package/dist/skills/tools/save_scenario.md +50 -0
  50. package/dist/skills/tools/verify_forecast.md +43 -0
  51. package/dist/src/config.d.ts +20 -0
  52. package/dist/src/config.js +44 -0
  53. package/dist/src/contracts/provenance.d.ts +37 -0
  54. package/dist/src/contracts/provenance.js +71 -0
  55. package/dist/src/contracts/tool-contract-registry.d.ts +43 -0
  56. package/dist/src/contracts/tool-contract-registry.js +282 -0
  57. package/dist/src/local/cloud-compute-client.d.ts +55 -0
  58. package/dist/src/local/cloud-compute-client.js +135 -0
  59. package/dist/src/local/encryption.d.ts +24 -0
  60. package/dist/src/local/encryption.js +105 -0
  61. package/dist/src/local/keychain.d.ts +41 -0
  62. package/dist/src/local/keychain.js +236 -0
  63. package/dist/src/local/local-config.d.ts +34 -0
  64. package/dist/src/local/local-config.js +61 -0
  65. package/dist/src/local/local-data-layer.d.ts +20 -0
  66. package/dist/src/local/local-data-layer.js +15 -0
  67. package/dist/src/local/local-plan-store.d.ts +66 -0
  68. package/dist/src/local/local-plan-store.js +195 -0
  69. package/dist/src/local/pii-scrubber.d.ts +26 -0
  70. package/dist/src/local/pii-scrubber.js +219 -0
  71. package/dist/src/policy/policy-engine.d.ts +44 -0
  72. package/dist/src/policy/policy-engine.js +119 -0
  73. package/dist/src/rate-limit.d.ts +17 -0
  74. package/dist/src/rate-limit.js +41 -0
  75. package/dist/src/security.d.ts +19 -0
  76. package/dist/src/security.js +118 -0
  77. package/dist/src/skills/index.d.ts +12 -0
  78. package/dist/src/skills/index.js +16 -0
  79. package/dist/src/skills/retirement-pack-v1.d.ts +28 -0
  80. package/dist/src/skills/retirement-pack-v1.js +295 -0
  81. package/dist/src/skills/skill-executor.d.ts +65 -0
  82. package/dist/src/skills/skill-executor.js +174 -0
  83. package/dist/src/skills/skill-manifest-schema.d.ts +337 -0
  84. package/dist/src/skills/skill-manifest-schema.js +94 -0
  85. package/dist/src/skills/skill-registry.d.ts +71 -0
  86. package/dist/src/skills/skill-registry.js +116 -0
  87. package/dist/src/telemetry.d.ts +12 -0
  88. package/dist/src/telemetry.js +59 -0
  89. package/dist/src/types.d.ts +46 -0
  90. package/dist/src/types.js +4 -0
  91. package/dist/tools/agent-tools.d.ts +12 -0
  92. package/dist/tools/agent-tools.js +141 -0
  93. package/dist/tools/forecast-management-tools.d.ts +9 -0
  94. package/dist/tools/forecast-management-tools.js +133 -0
  95. package/dist/tools/local-plan-tools.d.ts +8 -0
  96. package/dist/tools/local-plan-tools.js +357 -0
  97. package/dist/tools/mcp-helpers.d.ts +52 -0
  98. package/dist/tools/mcp-helpers.js +177 -0
  99. package/dist/tools/medicare-tools.d.ts +3 -0
  100. package/dist/tools/medicare-tools.js +162 -0
  101. package/dist/tools/optimize-roth-tools-test.d.ts +2 -0
  102. package/dist/tools/optimize-roth-tools-test.js +36 -0
  103. package/dist/tools/optimize-roth-tools.d.ts +3 -0
  104. package/dist/tools/optimize-roth-tools.js +818 -0
  105. package/dist/tools/plan-management-tools.d.ts +3 -0
  106. package/dist/tools/plan-management-tools.js +196 -0
  107. package/dist/tools/planning-tools.d.ts +3 -0
  108. package/dist/tools/planning-tools.js +290 -0
  109. package/dist/tools/proposal-tools.d.ts +3 -0
  110. package/dist/tools/proposal-tools.js +428 -0
  111. package/dist/tools/report-tools.d.ts +3 -0
  112. package/dist/tools/report-tools.js +245 -0
  113. package/dist/tools/scenario-management-tools.d.ts +3 -0
  114. package/dist/tools/scenario-management-tools.js +136 -0
  115. package/dist/views/verification-packet.html +211 -0
  116. package/host-configs/cowork.json +11 -0
  117. package/host-configs/goose.yaml +22 -0
  118. package/host-configs/openclaw-manifest.json +16 -0
  119. package/package.json +66 -0
  120. package/skills/SKILL.md +162 -0
  121. package/skills/manifest.json +51 -0
  122. package/skills/tools/activate_plan.md +36 -0
  123. package/skills/tools/coach.md +59 -0
  124. package/skills/tools/comprehensive_plan.md +65 -0
  125. package/skills/tools/create_plan.md +59 -0
  126. package/skills/tools/create_saved_plan.md +49 -0
  127. package/skills/tools/delete_plan.md +42 -0
  128. package/skills/tools/delete_scenario.md +38 -0
  129. package/skills/tools/generate_proposal.md +63 -0
  130. package/skills/tools/generate_retirement_report.md +50 -0
  131. package/skills/tools/get_active_plan.md +44 -0
  132. package/skills/tools/get_baseline_forecast.md +47 -0
  133. package/skills/tools/get_plan.md +44 -0
  134. package/skills/tools/get_plan_components.md +50 -0
  135. package/skills/tools/get_scenario.md +46 -0
  136. package/skills/tools/list_plans.md +44 -0
  137. package/skills/tools/list_scenarios.md +42 -0
  138. package/skills/tools/medicare-guardian.md +59 -0
  139. package/skills/tools/nestpilot_run_plan.md +61 -0
  140. package/skills/tools/optimize_roth_conversion.md +107 -0
  141. package/skills/tools/optimize_ss_claiming.md +30 -0
  142. package/skills/tools/rename_plan.md +34 -0
  143. package/skills/tools/retirement-planner.md +55 -0
  144. package/skills/tools/run_forecast.md +65 -0
  145. package/skills/tools/run_saved_forecast.md +52 -0
  146. package/skills/tools/run_scenario.md +66 -0
  147. package/skills/tools/save_plan.md +48 -0
  148. package/skills/tools/save_scenario.md +50 -0
  149. package/skills/tools/verify_forecast.md +43 -0
@@ -0,0 +1,66 @@
1
+ import type { EncryptionService } from "./encryption.js";
2
+ export interface LocalPlan {
3
+ id: string;
4
+ version: number;
5
+ created: string;
6
+ updated: string;
7
+ plan: Record<string, unknown>;
8
+ forecasts: Record<string, unknown>[];
9
+ scenarios: Record<string, unknown>[];
10
+ metadata: {
11
+ source: "local" | "imported" | "cloud-sync";
12
+ encryptionVersion: number;
13
+ };
14
+ }
15
+ export interface PlanSummary {
16
+ id: string;
17
+ updated: string;
18
+ summary: string;
19
+ }
20
+ export declare class LocalPlanStore {
21
+ private dataDir;
22
+ private encryption;
23
+ private plansPath;
24
+ private forecastsPath;
25
+ private scenariosPath;
26
+ constructor(dataDir: string, encryption: EncryptionService);
27
+ /**
28
+ * Ensures the data directory structure exists.
29
+ */
30
+ initialize(): Promise<void>;
31
+ /**
32
+ * Creates a new plan and persists it encrypted to disk.
33
+ */
34
+ create(plan: Record<string, unknown>): Promise<LocalPlan>;
35
+ /**
36
+ * Lists all stored plans with summary information.
37
+ */
38
+ list(): Promise<PlanSummary[]>;
39
+ /**
40
+ * Retrieves a specific plan by ID.
41
+ */
42
+ get(planId: string): Promise<LocalPlan>;
43
+ /**
44
+ * Updates an existing plan.
45
+ */
46
+ save(planId: string, plan: Record<string, unknown>): Promise<LocalPlan>;
47
+ /**
48
+ * Deletes a plan and its associated cached data.
49
+ */
50
+ delete(planId: string): Promise<void>;
51
+ /**
52
+ * Caches a forecast result for offline access.
53
+ */
54
+ cacheForecast(planId: string, forecast: Record<string, unknown>): Promise<void>;
55
+ /**
56
+ * Retrieves a cached forecast result, or null if none exists.
57
+ */
58
+ getCachedForecast(planId: string): Promise<Record<string, unknown> | null>;
59
+ /**
60
+ * Caches a scenario result.
61
+ */
62
+ cacheScenario(planId: string, label: string, scenario: Record<string, unknown>): Promise<void>;
63
+ private writePlan;
64
+ private readPlanFile;
65
+ private buildSummary;
66
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Local Plan Store — encrypted file-based CRUD for retirement plans.
3
+ *
4
+ * All plan data lives under `~/.nestpilot/plans/{uuid}.json` and is
5
+ * encrypted at rest using AES-256-GCM. Forecast results are cached
6
+ * under `~/.nestpilot/forecasts/{uuid}.json` for offline access.
7
+ *
8
+ * This module is only active when NESTPILOT_MODE=local.
9
+ *
10
+ * @feature FEAT-0087
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import fs from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { forecastsDir, plansDir, scenariosDir } from "./local-config.js";
16
+ // ── LocalPlanStore ───────────────────────────────────────────────────────
17
+ export class LocalPlanStore {
18
+ dataDir;
19
+ encryption;
20
+ plansPath;
21
+ forecastsPath;
22
+ scenariosPath;
23
+ constructor(dataDir, encryption) {
24
+ this.dataDir = dataDir;
25
+ this.encryption = encryption;
26
+ this.plansPath = plansDir(dataDir);
27
+ this.forecastsPath = forecastsDir(dataDir);
28
+ this.scenariosPath = scenariosDir(dataDir);
29
+ }
30
+ /**
31
+ * Ensures the data directory structure exists.
32
+ */
33
+ async initialize() {
34
+ await fs.mkdir(this.plansPath, { recursive: true });
35
+ await fs.mkdir(this.forecastsPath, { recursive: true });
36
+ await fs.mkdir(this.scenariosPath, { recursive: true });
37
+ }
38
+ /**
39
+ * Creates a new plan and persists it encrypted to disk.
40
+ */
41
+ async create(plan) {
42
+ const now = new Date().toISOString();
43
+ const localPlan = {
44
+ id: randomUUID(),
45
+ version: 1,
46
+ created: now,
47
+ updated: now,
48
+ plan,
49
+ forecasts: [],
50
+ scenarios: [],
51
+ metadata: {
52
+ source: "local",
53
+ encryptionVersion: 1,
54
+ },
55
+ };
56
+ await this.writePlan(localPlan);
57
+ return localPlan;
58
+ }
59
+ /**
60
+ * Lists all stored plans with summary information.
61
+ */
62
+ async list() {
63
+ await this.initialize();
64
+ const files = await fs.readdir(this.plansPath);
65
+ const summaries = [];
66
+ for (const file of files) {
67
+ if (!file.endsWith(".json"))
68
+ continue;
69
+ try {
70
+ const localPlan = await this.readPlanFile(path.join(this.plansPath, file));
71
+ summaries.push({
72
+ id: localPlan.id,
73
+ updated: localPlan.updated,
74
+ summary: this.buildSummary(localPlan),
75
+ });
76
+ }
77
+ catch (e) {
78
+ console.warn(`[local-plan-store] Skipping corrupt plan file: ${file}`, e);
79
+ }
80
+ }
81
+ return summaries.sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime());
82
+ }
83
+ /**
84
+ * Retrieves a specific plan by ID.
85
+ */
86
+ async get(planId) {
87
+ const filePath = path.join(this.plansPath, `${planId}.json`);
88
+ try {
89
+ return await this.readPlanFile(filePath);
90
+ }
91
+ catch (e) {
92
+ if (e.code === "ENOENT") {
93
+ throw new Error(`Plan not found: ${planId}`);
94
+ }
95
+ throw e;
96
+ }
97
+ }
98
+ /**
99
+ * Updates an existing plan.
100
+ */
101
+ async save(planId, plan) {
102
+ const existing = await this.get(planId);
103
+ const updated = {
104
+ ...existing,
105
+ plan,
106
+ version: existing.version + 1,
107
+ updated: new Date().toISOString(),
108
+ };
109
+ await this.writePlan(updated);
110
+ return updated;
111
+ }
112
+ /**
113
+ * Deletes a plan and its associated cached data.
114
+ */
115
+ async delete(planId) {
116
+ const planFile = path.join(this.plansPath, `${planId}.json`);
117
+ const forecastFile = path.join(this.forecastsPath, `${planId}.json`);
118
+ const scenarioFile = path.join(this.scenariosPath, `${planId}.json`);
119
+ // Remove plan (required)
120
+ await fs.unlink(planFile).catch((e) => {
121
+ if (e.code !== "ENOENT")
122
+ throw e;
123
+ });
124
+ // Remove cached data (optional — may not exist)
125
+ await fs.unlink(forecastFile).catch(() => { });
126
+ await fs.unlink(scenarioFile).catch(() => { });
127
+ }
128
+ /**
129
+ * Caches a forecast result for offline access.
130
+ */
131
+ async cacheForecast(planId, forecast) {
132
+ const data = JSON.stringify({
133
+ planId,
134
+ cached: new Date().toISOString(),
135
+ forecast,
136
+ });
137
+ const encrypted = await this.encryption.encrypt(data);
138
+ await fs.mkdir(this.forecastsPath, { recursive: true });
139
+ await fs.writeFile(path.join(this.forecastsPath, `${planId}.json`), encrypted);
140
+ }
141
+ /**
142
+ * Retrieves a cached forecast result, or null if none exists.
143
+ */
144
+ async getCachedForecast(planId) {
145
+ try {
146
+ const encrypted = await fs.readFile(path.join(this.forecastsPath, `${planId}.json`));
147
+ const decrypted = await this.encryption.decrypt(encrypted);
148
+ const data = JSON.parse(decrypted);
149
+ return data.forecast;
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ /**
156
+ * Caches a scenario result.
157
+ */
158
+ async cacheScenario(planId, label, scenario) {
159
+ // Read existing scenarios for this plan
160
+ let existing = [];
161
+ try {
162
+ const encrypted = await fs.readFile(path.join(this.scenariosPath, `${planId}.json`));
163
+ const decrypted = await this.encryption.decrypt(encrypted);
164
+ existing = JSON.parse(decrypted);
165
+ }
166
+ catch {
167
+ // No existing scenarios
168
+ }
169
+ existing.push({ label, scenario });
170
+ const data = JSON.stringify(existing);
171
+ const encrypted = await this.encryption.encrypt(data);
172
+ await fs.mkdir(this.scenariosPath, { recursive: true });
173
+ await fs.writeFile(path.join(this.scenariosPath, `${planId}.json`), encrypted);
174
+ }
175
+ // ── Internal ──────────────────────────────────────────────────────────
176
+ async writePlan(localPlan) {
177
+ await this.initialize();
178
+ const data = JSON.stringify(localPlan, null, 2);
179
+ const encrypted = await this.encryption.encrypt(data);
180
+ await fs.writeFile(path.join(this.plansPath, `${localPlan.id}.json`), encrypted);
181
+ }
182
+ async readPlanFile(filePath) {
183
+ const encrypted = await fs.readFile(filePath);
184
+ const decrypted = await this.encryption.decrypt(encrypted);
185
+ return JSON.parse(decrypted);
186
+ }
187
+ buildSummary(plan) {
188
+ const p = plan.plan;
189
+ const age = p.currentAge ?? p.age ?? "?";
190
+ const retireAge = p.retirementAge ?? p.retireAge ?? "?";
191
+ const spending = p.targetMonthlySpending ?? "?";
192
+ const accountCount = Array.isArray(p.accounts) ? p.accounts.length : 0;
193
+ return `Age ${age}, retire at ${retireAge}, $${spending}/mo spending, ${accountCount} account(s)`;
194
+ }
195
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * PII Scrubber — strips personally identifiable information from plan
3
+ * payloads before they are sent to the NestPilot cloud compute API.
4
+ *
5
+ * The cloud only needs mathematical inputs (ages, dollar amounts, rates,
6
+ * account types). It never sees names, notes, labels, account numbers,
7
+ * institution names, Plaid tokens, email addresses, SSNs, etc.
8
+ *
9
+ * @feature FEAT-0087
10
+ */
11
+ export interface ScrubResult {
12
+ /** PII-free payload safe for cloud transmission. */
13
+ payload: Record<string, unknown>;
14
+ /** Fields that were removed during scrubbing. */
15
+ removedFields: string[];
16
+ }
17
+ /**
18
+ * Scrubs PII from a plan payload, keeping only the mathematical fields
19
+ * required for cloud compute.
20
+ */
21
+ export declare function scrubPII(plan: Record<string, unknown>): ScrubResult;
22
+ /**
23
+ * Deep-scans a payload to ensure no PII has leaked through.
24
+ * Returns `true` if the payload is PII-free.
25
+ */
26
+ export declare function validateNoPII(payload: Record<string, unknown>): boolean;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * PII Scrubber — strips personally identifiable information from plan
3
+ * payloads before they are sent to the NestPilot cloud compute API.
4
+ *
5
+ * The cloud only needs mathematical inputs (ages, dollar amounts, rates,
6
+ * account types). It never sees names, notes, labels, account numbers,
7
+ * institution names, Plaid tokens, email addresses, SSNs, etc.
8
+ *
9
+ * @feature FEAT-0087
10
+ */
11
+ // ── Allowed field sets ───────────────────────────────────────────────────
12
+ /** Top-level scalar/numeric fields safe for cloud compute. */
13
+ const ALLOWED_TOP_LEVEL = new Set([
14
+ "currentAge",
15
+ "retirementAge",
16
+ "horizonAge",
17
+ "targetMonthlySpending",
18
+ "socialSecurityFRA",
19
+ "socialSecurityClaimAge",
20
+ "safeWithdrawalRate",
21
+ "effectiveTaxRate",
22
+ "inflationRate",
23
+ "displayMode",
24
+ "withdrawalStrategy",
25
+ "filingStatus",
26
+ "planId",
27
+ "schemaVersion",
28
+ // Nested objects that get scrubbed recursively
29
+ "accounts",
30
+ "incomeStreams",
31
+ "spouse",
32
+ "socialSecurity",
33
+ "healthComponents",
34
+ "spendStreams",
35
+ "strategy",
36
+ "assumptions",
37
+ "goals",
38
+ "household",
39
+ "taxProfile",
40
+ // Scenario-specific
41
+ "plan",
42
+ "overrides",
43
+ "label",
44
+ "payload",
45
+ ]);
46
+ /** Account object fields safe for cloud compute. */
47
+ const ALLOWED_ACCOUNT_FIELDS = new Set([
48
+ "type",
49
+ "balance",
50
+ "annualContribution",
51
+ "realReturn",
52
+ "employerMatch",
53
+ "employerMatchLimit",
54
+ "enabled",
55
+ "id",
56
+ ]);
57
+ /** Income stream fields safe for cloud compute. */
58
+ const ALLOWED_INCOME_FIELDS = new Set([
59
+ "type",
60
+ "amountMonthly",
61
+ "startAge",
62
+ "endAge",
63
+ "enabled",
64
+ "id",
65
+ "cola",
66
+ ]);
67
+ /** Spouse object fields safe for cloud compute. */
68
+ const ALLOWED_SPOUSE_FIELDS = new Set([
69
+ "age",
70
+ "retireAge",
71
+ "currentAge",
72
+ "retirementAge",
73
+ "salary",
74
+ "socialSecurity",
75
+ "accounts",
76
+ "incomeStreams",
77
+ ]);
78
+ /** Social Security fields safe for cloud compute. */
79
+ const ALLOWED_SS_FIELDS = new Set([
80
+ "fraBenefitMonthly",
81
+ "claimAge",
82
+ "enabled",
83
+ ]);
84
+ /** Known enum values — kept as-is during validation. */
85
+ const KNOWN_ENUM_VALUES = new Set([
86
+ // Account types
87
+ "traditional",
88
+ "roth",
89
+ "taxable",
90
+ "hsa",
91
+ "cash",
92
+ // Income types
93
+ "salary",
94
+ "social_security",
95
+ "pension",
96
+ "annuity",
97
+ "rental",
98
+ "other",
99
+ // Filing status
100
+ "single",
101
+ "married_filing_jointly",
102
+ "married_filing_separately",
103
+ "head_of_household",
104
+ // Withdrawal strategy
105
+ "taxable_first",
106
+ "traditional_first",
107
+ // Display mode
108
+ "real",
109
+ "nominal",
110
+ // Stress test
111
+ "EQUITY",
112
+ "ALL",
113
+ "PLAN_START",
114
+ "RETIREMENT",
115
+ "CUSTOM_YEAR",
116
+ "2008_CRISIS",
117
+ "1929_CRASH",
118
+ "DOT_COM_BUST",
119
+ ]);
120
+ // ── PII pattern detection ────────────────────────────────────────────────
121
+ const PII_PATTERNS = [
122
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, // email
123
+ /\b\d{3}-\d{2}-\d{4}\b/, // SSN
124
+ /\b\d{3}\.\d{2}\.\d{4}\b/, // SSN variant
125
+ /\(\d{3}\)\s?\d{3}-\d{4}/, // phone (xxx) xxx-xxxx
126
+ /\b\d{3}-\d{3}-\d{4}\b/, // phone xxx-xxx-xxxx
127
+ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // credit card
128
+ /\b\d{9,17}\b/, // account numbers (9-17 digits)
129
+ ];
130
+ /** Max string length heuristic — names/notes tend to be longer. */
131
+ const MAX_SAFE_STRING_LENGTH = 50;
132
+ // ── Scrubbing logic ──────────────────────────────────────────────────────
133
+ /**
134
+ * Scrubs PII from a plan payload, keeping only the mathematical fields
135
+ * required for cloud compute.
136
+ */
137
+ export function scrubPII(plan) {
138
+ const removedFields = [];
139
+ const payload = scrubObject(plan, ALLOWED_TOP_LEVEL, "", removedFields);
140
+ return { payload, removedFields };
141
+ }
142
+ function scrubObject(obj, allowedKeys, parentPath, removed) {
143
+ const result = {};
144
+ for (const [key, value] of Object.entries(obj)) {
145
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
146
+ if (!allowedKeys.has(key)) {
147
+ removed.push(fieldPath);
148
+ continue;
149
+ }
150
+ if (value === null || value === undefined) {
151
+ result[key] = value;
152
+ continue;
153
+ }
154
+ if (Array.isArray(value)) {
155
+ result[key] = scrubArray(key, value, fieldPath, removed);
156
+ }
157
+ else if (typeof value === "object") {
158
+ result[key] = scrubNestedObject(key, value, fieldPath, removed);
159
+ }
160
+ else {
161
+ // Primitives — keep numbers/booleans, validate strings
162
+ result[key] = value;
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+ function scrubArray(key, arr, parentPath, removed) {
168
+ if (key === "accounts") {
169
+ return arr.map((item, i) => typeof item === "object" && item !== null
170
+ ? scrubObject(item, ALLOWED_ACCOUNT_FIELDS, `${parentPath}[${i}]`, removed)
171
+ : item);
172
+ }
173
+ if (key === "incomeStreams") {
174
+ return arr.map((item, i) => typeof item === "object" && item !== null
175
+ ? scrubObject(item, ALLOWED_INCOME_FIELDS, `${parentPath}[${i}]`, removed)
176
+ : item);
177
+ }
178
+ // Generic array — pass through (for component arrays, etc.)
179
+ return arr;
180
+ }
181
+ function scrubNestedObject(key, obj, parentPath, removed) {
182
+ if (key === "spouse") {
183
+ return scrubObject(obj, ALLOWED_SPOUSE_FIELDS, parentPath, removed);
184
+ }
185
+ if (key === "socialSecurity") {
186
+ return scrubObject(obj, ALLOWED_SS_FIELDS, parentPath, removed);
187
+ }
188
+ // For nested objects like 'household', 'goals', 'overrides', 'strategy',
189
+ // 'assumptions', 'taxProfile' — recurse with ALLOWED_TOP_LEVEL since
190
+ // these may contain nested plan-like structures
191
+ return scrubObject(obj, ALLOWED_TOP_LEVEL, parentPath, removed);
192
+ }
193
+ // ── Validation ───────────────────────────────────────────────────────────
194
+ /**
195
+ * Deep-scans a payload to ensure no PII has leaked through.
196
+ * Returns `true` if the payload is PII-free.
197
+ */
198
+ export function validateNoPII(payload) {
199
+ return !containsPII(payload);
200
+ }
201
+ function containsPII(value) {
202
+ if (typeof value === "string") {
203
+ // Allow known enum values
204
+ if (KNOWN_ENUM_VALUES.has(value))
205
+ return false;
206
+ // Flag long strings that aren't enum values
207
+ if (value.length > MAX_SAFE_STRING_LENGTH)
208
+ return true;
209
+ // Check against PII patterns
210
+ return PII_PATTERNS.some((pattern) => pattern.test(value));
211
+ }
212
+ if (Array.isArray(value)) {
213
+ return value.some(containsPII);
214
+ }
215
+ if (typeof value === "object" && value !== null) {
216
+ return Object.values(value).some(containsPII);
217
+ }
218
+ return false;
219
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Policy Engine Adapter — FEAT-0056
3
+ *
4
+ * Evaluates role/tool/action permissions against the policy baseline
5
+ * before tool execution. Returns machine-readable policy decisions
6
+ * with explicit deny reasons.
7
+ *
8
+ * @feature FEAT-0056
9
+ */
10
+ export type PolicyDecisionOutcome = "allow" | "deny";
11
+ export interface PolicyDecision {
12
+ decisionId: string;
13
+ actorId: string;
14
+ toolName: string;
15
+ decision: PolicyDecisionOutcome;
16
+ reasonCodes: string[];
17
+ evaluatedAt: string;
18
+ }
19
+ export type UserRole = "anonymous" | "authenticated" | "advisor" | "admin";
20
+ export interface PolicyContext {
21
+ actorId: string;
22
+ role: UserRole;
23
+ toolName: string;
24
+ }
25
+ export declare const REASON: {
26
+ readonly TOOL_NOT_IN_ALLOWLIST: "TOOL_NOT_IN_ALLOWLIST";
27
+ readonly ROLE_INSUFFICIENT: "ROLE_INSUFFICIENT";
28
+ readonly PRIVILEGED_TOOL_REQUIRES_ADVISOR: "PRIVILEGED_TOOL_REQUIRES_ADVISOR";
29
+ readonly TOOL_UNREGISTERED: "TOOL_UNREGISTERED";
30
+ };
31
+ /**
32
+ * Evaluates whether the given actor/role may invoke the specified tool.
33
+ * Returns a machine-readable PolicyDecision with explicit deny reasons.
34
+ */
35
+ export declare function evaluatePolicy(ctx: PolicyContext): PolicyDecision;
36
+ /**
37
+ * Convenience: evaluates policy and throws if denied.
38
+ * Returns the allow decision for provenance/audit purposes.
39
+ */
40
+ export declare function requirePolicy(ctx: PolicyContext): PolicyDecision;
41
+ /**
42
+ * Resets the decision counter (for testing only).
43
+ */
44
+ export declare function _resetDecisionCounter(): void;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Policy Engine Adapter — FEAT-0056
3
+ *
4
+ * Evaluates role/tool/action permissions against the policy baseline
5
+ * before tool execution. Returns machine-readable policy decisions
6
+ * with explicit deny reasons.
7
+ *
8
+ * @feature FEAT-0056
9
+ */
10
+ // ── Reason codes ─────────────────────────────────────────────────────────
11
+ export const REASON = {
12
+ TOOL_NOT_IN_ALLOWLIST: "TOOL_NOT_IN_ALLOWLIST",
13
+ ROLE_INSUFFICIENT: "ROLE_INSUFFICIENT",
14
+ PRIVILEGED_TOOL_REQUIRES_ADVISOR: "PRIVILEGED_TOOL_REQUIRES_ADVISOR",
15
+ TOOL_UNREGISTERED: "TOOL_UNREGISTERED",
16
+ };
17
+ // ── Role-based tool allowlists ───────────────────────────────────────────
18
+ const ROLE_ALLOWLISTS = {
19
+ admin: "all",
20
+ advisor: "all",
21
+ authenticated: new Set([
22
+ "create_plan",
23
+ "run_forecast",
24
+ "run_scenario",
25
+ "verify_forecast",
26
+ "coach",
27
+ "medicare-guardian",
28
+ "medicare-analyze",
29
+ "retirement-planner",
30
+ "email-subscribe",
31
+ "optimize_roth_conversion",
32
+ // plan management
33
+ "list_plans",
34
+ "get_plan",
35
+ "get_active_plan",
36
+ "create_saved_plan",
37
+ "save_plan",
38
+ "activate_plan",
39
+ "delete_plan",
40
+ "rename_plan",
41
+ "get_plan_components",
42
+ // forecast management
43
+ "run_saved_forecast",
44
+ "get_baseline_forecast",
45
+ // scenario management
46
+ "save_scenario",
47
+ "list_scenarios",
48
+ "get_scenario",
49
+ "delete_scenario",
50
+ // comprehensive
51
+ "comprehensive_plan",
52
+ ]),
53
+ anonymous: new Set([
54
+ "create_plan",
55
+ "run_forecast",
56
+ "coach",
57
+ "medicare-guardian",
58
+ "medicare-analyze",
59
+ "retirement-planner",
60
+ ]),
61
+ };
62
+ // Tools that require at least advisor role
63
+ const PRIVILEGED_TOOLS = new Set([
64
+ // Currently none are privileged-only, but the gate is ready.
65
+ // Future: tools that can execute transactions or modify advisor records.
66
+ ]);
67
+ // ── Policy evaluation ────────────────────────────────────────────────────
68
+ let decisionCounter = 0;
69
+ function generateDecisionId() {
70
+ decisionCounter += 1;
71
+ return `pd-${Date.now()}-${decisionCounter}`;
72
+ }
73
+ /**
74
+ * Evaluates whether the given actor/role may invoke the specified tool.
75
+ * Returns a machine-readable PolicyDecision with explicit deny reasons.
76
+ */
77
+ export function evaluatePolicy(ctx) {
78
+ const reasonCodes = [];
79
+ // Check privileged tool gate
80
+ if (PRIVILEGED_TOOLS.has(ctx.toolName)) {
81
+ if (ctx.role !== "advisor" && ctx.role !== "admin") {
82
+ reasonCodes.push(REASON.PRIVILEGED_TOOL_REQUIRES_ADVISOR);
83
+ }
84
+ }
85
+ // Check role allowlist
86
+ const allowlist = ROLE_ALLOWLISTS[ctx.role];
87
+ if (allowlist !== "all" && !allowlist.has(ctx.toolName)) {
88
+ reasonCodes.push(REASON.TOOL_NOT_IN_ALLOWLIST);
89
+ }
90
+ const decision = reasonCodes.length === 0 ? "allow" : "deny";
91
+ return {
92
+ decisionId: generateDecisionId(),
93
+ actorId: ctx.actorId,
94
+ toolName: ctx.toolName,
95
+ decision,
96
+ reasonCodes,
97
+ evaluatedAt: new Date().toISOString(),
98
+ };
99
+ }
100
+ /**
101
+ * Convenience: evaluates policy and throws if denied.
102
+ * Returns the allow decision for provenance/audit purposes.
103
+ */
104
+ export function requirePolicy(ctx) {
105
+ const decision = evaluatePolicy(ctx);
106
+ if (decision.decision === "deny") {
107
+ const err = new Error(`Policy denied: tool '${ctx.toolName}' for role '${ctx.role}' — [${decision.reasonCodes.join(", ")}]`);
108
+ err.policyDecision =
109
+ decision;
110
+ throw err;
111
+ }
112
+ return decision;
113
+ }
114
+ /**
115
+ * Resets the decision counter (for testing only).
116
+ */
117
+ export function _resetDecisionCounter() {
118
+ decisionCounter = 0;
119
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * In-memory rate limiter — sliding window per key.
3
+ * Adapted from mcp-edge.
4
+ */
5
+ export interface RateLimitDecision {
6
+ allowed: boolean;
7
+ remaining: number;
8
+ resetAtMs: number;
9
+ retryAfterSeconds: number;
10
+ }
11
+ export declare class InMemoryRateLimiter {
12
+ private readonly windowMs;
13
+ private readonly maxRequests;
14
+ private readonly buckets;
15
+ constructor(windowMs: number, maxRequests: number);
16
+ check(key: string, nowMs?: number): RateLimitDecision;
17
+ }