mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
Merge branch 'main' into feature/retirement-planning
This commit is contained in:
60
.github/workflows/label-not-gittensor.yml
vendored
Normal file
60
.github/workflows/label-not-gittensor.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Label non-Gittensor PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add not-gittensor label for matched authors
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GITTENSOR_USERS: ${{ vars.GITTENSOR_USERS || '[]' }}
|
||||
GITTENSOR_EXCEPTIONS: ${{ vars.GITTENSOR_EXCEPTIONS || '[]' }}
|
||||
TARGET_LABEL: not-gittensor
|
||||
with:
|
||||
script: |
|
||||
const parseList = (raw, name) => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || '[]');
|
||||
if (!Array.isArray(parsed)) {
|
||||
core.setFailed(`${name} must be a JSON array.`);
|
||||
return [];
|
||||
}
|
||||
return parsed.map((value) => String(value).toLowerCase());
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to parse ${name}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const author = context.payload.pull_request.user.login.toLowerCase();
|
||||
const users = new Set(parseList(process.env.GITTENSOR_USERS, 'GITTENSOR_USERS'));
|
||||
const exceptions = new Set(parseList(process.env.GITTENSOR_EXCEPTIONS, 'GITTENSOR_EXCEPTIONS'));
|
||||
|
||||
if (users.has(author) || exceptions.has(author)) {
|
||||
core.info(`No label needed for @${author}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingLabels = context.payload.pull_request.labels.map((label) => label.name);
|
||||
if (existingLabels.includes(process.env.TARGET_LABEL)) {
|
||||
core.info(`Label ${process.env.TARGET_LABEL} already present.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: [process.env.TARGET_LABEL],
|
||||
});
|
||||
|
||||
core.info(`Added ${process.env.TARGET_LABEL} to PR #${context.payload.pull_request.number}.`);
|
||||
6
.github/workflows/preview-deploy.yml
vendored
6
.github/workflows/preview-deploy.yml
vendored
@@ -2,7 +2,7 @@ name: Deploy PR Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- 'charts/**'
|
||||
- 'docs/**'
|
||||
@@ -10,7 +10,9 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy-preview:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'preview-cf')
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'preview-cf') &&
|
||||
(github.event.action != 'labeled' || github.event.label.name == 'preview-cf')
|
||||
name: Deploy to Cloudflare Containers
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.7.1-alpha.10
|
||||
0.7.1-alpha.11
|
||||
|
||||
@@ -9,7 +9,7 @@ WORKDIR /rails
|
||||
|
||||
# Install base packages
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2 procps \
|
||||
&& apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2 procps libjemalloc2 \
|
||||
&& rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Set production environment
|
||||
@@ -19,7 +19,7 @@ ENV RAILS_ENV="production" \
|
||||
BUNDLE_PATH="/usr/local/bundle" \
|
||||
BUNDLE_WITHOUT="development" \
|
||||
BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA}
|
||||
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base AS build
|
||||
|
||||
|
||||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
@@ -5,8 +5,8 @@ class FamilyMerchantsController < ApplicationController
|
||||
@breadcrumbs = [ [ t("breadcrumbs.home"), root_path ], [ t("breadcrumbs.merchants"), nil ] ]
|
||||
|
||||
# Show all merchants for this family
|
||||
@family_merchants = Current.family.merchants.alphabetically
|
||||
@provider_merchants = Current.family.assigned_merchants_for(Current.user).where(type: "ProviderMerchant").alphabetically
|
||||
@all_family_merchants = Current.family.merchants.alphabetically
|
||||
@all_provider_merchants = Current.family.assigned_merchants_for(Current.user).where(type: "ProviderMerchant").alphabetically
|
||||
|
||||
# Show recently unlinked ProviderMerchants (within last 30 days)
|
||||
# Exclude merchants that are already assigned to transactions (they appear in provider_merchants)
|
||||
@@ -14,12 +14,15 @@ class FamilyMerchantsController < ApplicationController
|
||||
.where(family: Current.family)
|
||||
.recently_unlinked
|
||||
.pluck(:merchant_id)
|
||||
assigned_ids = @provider_merchants.pluck(:id)
|
||||
assigned_ids = @all_provider_merchants.pluck(:id)
|
||||
@unlinked_merchants = ProviderMerchant.where(id: recently_unlinked_ids - assigned_ids).alphabetically
|
||||
|
||||
@enhanceable_count = @provider_merchants.where(website_url: [ nil, "" ]).count
|
||||
@enhanceable_count = @all_provider_merchants.where(website_url: [ nil, "" ]).count
|
||||
@llm_available = Provider::Registry.get_provider(:openai).present?
|
||||
|
||||
@pagy_family_merchants, @family_merchants = pagy(@all_family_merchants, page_param: :family_page, limit: safe_per_page)
|
||||
@pagy_provider_merchants, @provider_merchants = pagy(@all_provider_merchants, page_param: :provider_page, limit: safe_per_page)
|
||||
|
||||
render layout: "settings"
|
||||
end
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ class MessagesController < ApplicationController
|
||||
before_action :set_chat
|
||||
|
||||
def create
|
||||
@message = UserMessage.create!(
|
||||
@message = UserMessage.new(
|
||||
chat: @chat,
|
||||
content: message_params[:content],
|
||||
ai_model: message_params[:ai_model].presence || Chat.default_model
|
||||
)
|
||||
|
||||
redirect_to chat_path(@chat, thinking: true)
|
||||
if @message.save
|
||||
redirect_to chat_path(@chat, thinking: true)
|
||||
else
|
||||
redirect_to chat_path(@chat), alert: @message.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -200,6 +200,12 @@ class Settings::HostingsController < ApplicationController
|
||||
|
||||
redirect_to settings_hosting_path, notice: t(".success")
|
||||
rescue Setting::ValidationError => error
|
||||
# Preserve user-submitted OpenAI config so the form re-renders with their
|
||||
# input intact (issue #1824). The form auto-submits on blur, so a partial
|
||||
# entry (e.g. URI base before model) hits validation and would otherwise
|
||||
# be wiped because the view reads from the unchanged Setting.* values.
|
||||
@openai_uri_base_input = hosting_params[:openai_uri_base] if hosting_params.key?(:openai_uri_base)
|
||||
@openai_model_input = hosting_params[:openai_model] if hosting_params.key?(:openai_model)
|
||||
flash.now[:alert] = error.message
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ class TradesController < ApplicationController
|
||||
format.turbo_stream { stream_redirect_back_or_to account_path(@account) }
|
||||
end
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
render :new, status: :unprocessable_entity, formats: [ :html ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,7 +69,7 @@ class TradesController < ApplicationController
|
||||
end
|
||||
end
|
||||
else
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_entity, formats: [ :html ]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -27,21 +27,27 @@ class UsersController < ApplicationController
|
||||
end
|
||||
else
|
||||
was_ai_enabled = @user.ai_enabled
|
||||
@user.update!(user_params.except(:redirect_to, :delete_profile_image))
|
||||
@user.profile_image.purge if should_purge_profile_image?
|
||||
if @user.update(user_params.except(:redirect_to, :delete_profile_image))
|
||||
@user.profile_image.purge if should_purge_profile_image?
|
||||
|
||||
# Add a special notice if AI was just enabled or disabled
|
||||
notice = if !was_ai_enabled && @user.ai_enabled
|
||||
"AI Assistant has been enabled successfully."
|
||||
elsif was_ai_enabled && !@user.ai_enabled
|
||||
"AI Assistant has been disabled."
|
||||
# Add a special notice if AI was just enabled or disabled
|
||||
notice = if !was_ai_enabled && @user.ai_enabled
|
||||
"AI Assistant has been enabled successfully."
|
||||
elsif was_ai_enabled && !@user.ai_enabled
|
||||
"AI Assistant has been disabled."
|
||||
else
|
||||
t(".success")
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { handle_redirect(notice) }
|
||||
format.json { head :ok }
|
||||
end
|
||||
else
|
||||
t(".success")
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { handle_redirect(notice) }
|
||||
format.json { head :ok }
|
||||
respond_to do |format|
|
||||
format.html { redirect_to settings_profile_path, alert: @user.errors.full_messages.to_sentence }
|
||||
format.json { render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,9 +7,9 @@ export default class extends Controller {
|
||||
const query = this.inputTarget.value.toLocaleLowerCase().trim();
|
||||
let visibleCount = 0;
|
||||
|
||||
this.itemTargets.forEach(item => {
|
||||
const name = item.dataset.bankName?.toLocaleLowerCase() ?? "";
|
||||
const match = name.includes(query);
|
||||
this.itemTargets.forEach((item) => {
|
||||
const haystack = (item.dataset.bankSearch ?? "").toLocaleLowerCase();
|
||||
const match = haystack.includes(query);
|
||||
item.style.display = match ? "" : "none";
|
||||
if (match) visibleCount++;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ class Account < ApplicationRecord
|
||||
include AASM, Syncable, Monetizable, Chartable, Linkable, Enrichable, Anchorable, Reconcileable, TaxTreatable
|
||||
|
||||
before_validation :assign_default_owner, if: -> { owner_id.blank? }
|
||||
|
||||
before_destroy :capture_account_statement_ids_to_move
|
||||
before_destroy :cleanup_transfers
|
||||
|
||||
after_destroy_commit :move_account_statements_to_inbox
|
||||
|
||||
validates :name, :balance, :currency, presence: true
|
||||
@@ -543,4 +546,12 @@ class Account < ApplicationRecord
|
||||
updated_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
def cleanup_transfers
|
||||
transaction_ids = entries.where(entryable_type: "Transaction").pluck(:entryable_id)
|
||||
|
||||
transfers = Transfer.where(inflow_transaction_id: transaction_ids).or(Transfer.where(outflow_transaction_id: transaction_ids))
|
||||
|
||||
transfers.find_each(&:destroy!)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,8 +11,11 @@ class IbkrAccount::HistoricalBalancesSync
|
||||
return unless account.present?
|
||||
return if normalized_rows.empty?
|
||||
|
||||
rows = balance_rows
|
||||
return if rows.empty?
|
||||
|
||||
account.balances.upsert_all(
|
||||
balance_rows,
|
||||
rows,
|
||||
unique_by: %i[account_id date currency]
|
||||
)
|
||||
end
|
||||
@@ -109,12 +112,35 @@ class IbkrAccount::HistoricalBalancesSync
|
||||
|
||||
def balance_rows
|
||||
current_time = Time.current
|
||||
trade_flows_by_date # ensure @failed_fx_dates is populated before iterating
|
||||
|
||||
normalized_rows.each_with_index.map do |row, index|
|
||||
normalized_rows.each_with_index.filter_map do |row, index|
|
||||
next if @failed_fx_dates.include?(row[:date])
|
||||
previous_row = index.zero? ? nil : normalized_rows[index - 1]
|
||||
start_cash_balance = previous_row ? previous_row[:cash] : row[:cash]
|
||||
start_non_cash_balance = previous_row ? previous_row[:non_cash] : row[:non_cash]
|
||||
|
||||
# Derive market return directly from IBKR's equity data so Period Return
|
||||
# matches IBKR without requiring third-party security price providers.
|
||||
#
|
||||
# nmf = Δnon_cash - net_buy_sell
|
||||
# Δnon_cash : change in holdings value per IBKR equity summary (exact)
|
||||
# net_buy_sell: sum of trade entry amounts converted to base currency
|
||||
# (positive = buy, negative = sell; IBKR fx_rate_to_base applied)
|
||||
#
|
||||
# non_cash_adjustments absorbs net_buy_sell so the virtual column
|
||||
# end_non_cash_balance = start + nmf + adjustments stays equal to row[:non_cash].
|
||||
if previous_row
|
||||
net_buy_sell = trade_flows_by_date[row[:date]] || 0
|
||||
nmf = row[:non_cash] - start_non_cash_balance - net_buy_sell
|
||||
non_cash_adj = net_buy_sell
|
||||
else
|
||||
# First-day row has no prior period to diff against, so both values are
|
||||
# intentionally zero — not a bug, just an unavoidable bootstrap constraint.
|
||||
nmf = 0
|
||||
non_cash_adj = 0
|
||||
end
|
||||
|
||||
{
|
||||
account_id: account.id,
|
||||
date: row[:date],
|
||||
@@ -127,13 +153,44 @@ class IbkrAccount::HistoricalBalancesSync
|
||||
cash_outflows: 0,
|
||||
non_cash_inflows: 0,
|
||||
non_cash_outflows: 0,
|
||||
net_market_flows: 0,
|
||||
net_market_flows: nmf,
|
||||
cash_adjustments: row[:cash] - start_cash_balance,
|
||||
non_cash_adjustments: row[:non_cash] - start_non_cash_balance,
|
||||
non_cash_adjustments: non_cash_adj,
|
||||
flows_factor: 1,
|
||||
created_at: current_time,
|
||||
updated_at: current_time
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Net value of all trades on each date, in account base currency.
|
||||
# Uses the IBKR-provided fx_rate_to_base stored on each Trade entry so the
|
||||
# conversion is exact and consistent with IBKR's own calculations.
|
||||
# Positive = net buy (cash out), negative = net sell (cash in).
|
||||
def trade_flows_by_date
|
||||
@trade_flows_by_date ||= begin
|
||||
@failed_fx_dates = []
|
||||
if account
|
||||
account.entries
|
||||
.joins("INNER JOIN trades ON trades.id = entries.entryable_id AND entries.entryable_type = 'Trade'")
|
||||
.where.not(trades: { qty: 0 })
|
||||
.includes(:entryable)
|
||||
.each_with_object(Hash.new(0)) do |entry, flows|
|
||||
custom_rate = entry.entryable.exchange_rate
|
||||
base_amount = Money.new(entry.amount, entry.currency)
|
||||
.exchange_to(account_currency, custom_rate: custom_rate, date: entry.date)
|
||||
.amount
|
||||
flows[entry.date] += base_amount
|
||||
rescue Money::ConversionError
|
||||
Rails.logger.warn(
|
||||
"IbkrAccount::HistoricalBalancesSync - No FX rate for #{entry.currency}→#{account_currency} " \
|
||||
"on #{entry.date}; balance row for this date will not be persisted"
|
||||
)
|
||||
@failed_fx_dates << entry.date
|
||||
end
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,8 +38,16 @@ class Transfer < ApplicationRecord
|
||||
# Once transfer is destroyed, we need to mark the denormalized kind fields on the transactions
|
||||
def destroy!
|
||||
Transfer.transaction do
|
||||
inflow_transaction.update!(kind: "standard")
|
||||
outflow_transaction.update!(kind: "standard")
|
||||
[ inflow_transaction, outflow_transaction ].each do |transaction|
|
||||
next if transaction.nil?
|
||||
next unless Transaction.exists?(transaction.id)
|
||||
begin
|
||||
transaction.update!(kind: "standard")
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
rescue NoMethodError
|
||||
next
|
||||
end
|
||||
end
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,6 +51,7 @@ class User < ApplicationRecord
|
||||
validates :password, length: { minimum: 8 }, allow_nil: true
|
||||
normalizes :email, with: ->(email) { email.strip.downcase }
|
||||
normalizes :unconfirmed_email, with: ->(email) { email&.strip&.downcase }
|
||||
normalizes :locale, with: ->(locale) { locale.presence }
|
||||
|
||||
normalizes :first_name, :last_name, with: ->(value) { value.strip.presence }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= f.label :role, t(".filters.role"), class: "block text-sm font-medium text-primary mb-1" %>
|
||||
<%= f.select :role,
|
||||
options_for_select(
|
||||
[[t(".filters.role_all"), ""], [t(".roles.guest"), "guest"], [t(".roles.member", default: "Member"), "member"], [t(".roles.admin"), "admin"], [t(".roles.super_admin"), "super_admin"]],
|
||||
[[t(".filters.role_all"), ""], [t(".roles.guest"), "guest"], [t(".roles.member"), "member"], [t(".roles.admin"), "admin"], [t(".roles.super_admin"), "super_admin"]],
|
||||
params[:role]
|
||||
),
|
||||
{},
|
||||
@@ -75,7 +75,7 @@
|
||||
</span>
|
||||
<% elsif sub %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
|
||||
<%= sub.active? ? "bg-green-100 text-green-800" : "bg-surface text-secondary" %>">
|
||||
<%= sub.active? ? "bg-success/10 text-success" : "bg-surface text-secondary" %>">
|
||||
<%= sub.status.humanize %>
|
||||
</span>
|
||||
<% else %>
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
<div class="border-t border-primary">
|
||||
<table class="w-full">
|
||||
<thead class="bg-surface-default border-b border-primary">
|
||||
<thead class="bg-surface border-b border-primary">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-secondary uppercase"><%= t(".table.user") %></th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-secondary uppercase"><%= t(".table.last_login") %></th>
|
||||
@@ -123,7 +123,7 @@
|
||||
<%= form.select :role,
|
||||
options_for_select([
|
||||
[t(".roles.guest"), "guest"],
|
||||
[t(".roles.member", default: "Member"), "member"],
|
||||
[t(".roles.member"), "member"],
|
||||
[t(".roles.admin"), "admin"],
|
||||
[t(".roles.super_admin"), "super_admin"]
|
||||
], user.role),
|
||||
@@ -139,7 +139,7 @@
|
||||
<% if pending_invitations.any? %>
|
||||
<tbody class="divide-y divide-alpha-black-200 theme-dark:divide-alpha-white-200 border-t border-dashed border-primary">
|
||||
<% pending_invitations.each do |invitation| %>
|
||||
<tr class="bg-red-50/30 dark:bg-red-950/20">
|
||||
<tr class="bg-destructive/5">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<%= icon "mail", class: "w-5 h-5 text-secondary shrink-0" %>
|
||||
@@ -160,7 +160,7 @@
|
||||
<button type="submit"
|
||||
data-admin-invitation-delete-target="button"
|
||||
data-action="click->admin-invitation-delete#handleClick"
|
||||
class="text-sm text-red-600 hover:text-red-800 border border-red-300 rounded-lg px-2 py-1 hover:bg-red-50 transition-colors">
|
||||
class="text-sm text-destructive border border-destructive rounded-lg px-2 py-1 hover:bg-destructive/10 transition-colors">
|
||||
<%= t(".invitations.delete") %>
|
||||
</button>
|
||||
<% end %>
|
||||
@@ -198,9 +198,9 @@
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-surface text-primary shrink-0">
|
||||
<%= t(".roles.member", default: "Member") %>
|
||||
<%= t(".roles.member") %>
|
||||
</span>
|
||||
<p class="text-secondary"><%= t(".role_descriptions.member", default: "Basic user access. Can manage their own accounts, transactions, and settings.") %></p>
|
||||
<p class="text-secondary"><%= t(".role_descriptions.member") %></p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-surface text-primary shrink-0">
|
||||
@@ -209,7 +209,7 @@
|
||||
<p class="text-secondary"><%= t(".role_descriptions.admin") %></p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 shrink-0">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-success/10 text-success shrink-0">
|
||||
<%= t(".roles.super_admin") %>
|
||||
</span>
|
||||
<p class="text-secondary"><%= t(".role_descriptions.super_admin") %></p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="space-y-2 max-h-80 overflow-y-auto">
|
||||
<% @aspsps.each do |aspsp| %>
|
||||
<div data-bank-search-target="item" data-bank-name="<%= aspsp[:name].to_s.downcase(:fold) %>">
|
||||
<div data-bank-search-target="item" data-bank-search="<%= [ aspsp[:name], aspsp[:bic] ].compact_blank.join(" ").downcase(:fold) %>">
|
||||
<%= button_to authorize_enable_banking_item_path(@enable_banking_item),
|
||||
method: :post,
|
||||
params: { aspsp_name: aspsp[:name], new_connection: @new_connection },
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary uppercase">
|
||||
<p><%= t(".family_title", moniker: family_moniker) %></p>
|
||||
<span class="text-subdued">·</span>
|
||||
<p><%= @family_merchants.count %></p>
|
||||
<p><%= @all_family_merchants.count %></p>
|
||||
</div>
|
||||
|
||||
<% if @family_merchants.any? %>
|
||||
<% if @all_family_merchants.any? %>
|
||||
<div class="rounded-xl bg-container-inset space-y-1 p-1">
|
||||
<div class="bg-container rounded-lg shadow-border-xs overflow-x-auto">
|
||||
<table class="w-full">
|
||||
@@ -52,13 +52,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pt-4">
|
||||
<%= render "shared/pagination", pagy: @pagy_family_merchants %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary uppercase">
|
||||
<p><%= t(".provider_title") %></p>
|
||||
<span class="text-subdued">·</span>
|
||||
<p><%= @provider_merchants.count %></p>
|
||||
<p><%= @all_provider_merchants.count %></p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-container-inset border border-secondary rounded-lg">
|
||||
@@ -84,7 +88,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @provider_merchants.any? %>
|
||||
<% if @all_provider_merchants.any? %>
|
||||
<div class="rounded-xl bg-container-inset space-y-1 p-1">
|
||||
<div class="bg-container rounded-lg shadow-border-xs overflow-x-auto">
|
||||
<table class="w-full">
|
||||
@@ -106,6 +110,10 @@
|
||||
<p class="text-secondary text-sm text-center"><%= t(".provider_empty", moniker: family_moniker_downcase) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pt-4">
|
||||
<%= render "shared/pagination", pagy: @pagy_provider_merchants %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<% if @unlinked_merchants.any? %>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<%= t(".#{step_mapping_class.name.demodulize.underscore}_title", import_type: @import.type.underscore.humanize) %>
|
||||
</h1>
|
||||
<p class="text-secondary text-sm">
|
||||
<%= t(".#{step_mapping_class.name.demodulize.underscore}_description", import_type: @import.type.underscore.humanize, product: product_name) %>
|
||||
<%= t(".#{step_mapping_class.name.demodulize.underscore}_description", import_type: @import.type.underscore.humanize, product_name: product_name) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<%= render "imports/import_option",
|
||||
type: "TransactionImport",
|
||||
icon_name: "bar-chart-2",
|
||||
icon_bg_class: "bg-gray-tint-5",
|
||||
icon_bg_class: "bg-surface-inset",
|
||||
icon_text_class: "text-subdued",
|
||||
label: t(".import_ynab"),
|
||||
enabled: false,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<%= form.text_field :brand_fetch_client_id,
|
||||
label: t(".label"),
|
||||
type: "password",
|
||||
autocomplete: "new-password",
|
||||
placeholder: t(".placeholder"),
|
||||
value: ENV.fetch("BRAND_FETCH_CLIENT_ID", Setting.brand_fetch_client_id),
|
||||
disabled: ENV["BRAND_FETCH_CLIENT_ID"].present?,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<%= form.text_field :openai_uri_base,
|
||||
label: t(".uri_base_label"),
|
||||
placeholder: t(".uri_base_placeholder"),
|
||||
value: Setting.openai_uri_base,
|
||||
value: @openai_uri_base_input || Setting.openai_uri_base,
|
||||
autocomplete: "off",
|
||||
autocapitalize: "none",
|
||||
spellcheck: "false",
|
||||
@@ -41,7 +41,7 @@
|
||||
<%= form.text_field :openai_model,
|
||||
label: t(".model_label"),
|
||||
placeholder: t(".model_placeholder"),
|
||||
value: Setting.openai_model,
|
||||
value: @openai_model_input || Setting.openai_model,
|
||||
autocomplete: "off",
|
||||
autocapitalize: "none",
|
||||
spellcheck: "false",
|
||||
|
||||
@@ -17,18 +17,24 @@
|
||||
<%= tag.details open: open,
|
||||
class: "group bg-container shadow-border-xs rounded-xl #{border_class}",
|
||||
data: details_data do %>
|
||||
<summary class="flex items-center gap-3 min-h-15 px-4 py-3.5 cursor-pointer rounded-xl list-none [&::-webkit-details-marker]:hidden">
|
||||
<%= icon "chevron-right", size: "sm", class: "!w-3.5 !h-3.5 text-secondary group-open:rotate-90 transition-transform" %>
|
||||
<div class="flex items-center gap-2 flex-wrap min-w-0 flex-1">
|
||||
<h3 class="text-sm font-medium text-primary"><%= entry[:title] %></h3>
|
||||
<%= render "settings/providers/maturity_badge", label: maturity_lbl %>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0 group-open:hidden">
|
||||
<% if meta.present? %>
|
||||
<span class="text-xs text-subdued"><%= meta %></span>
|
||||
<% end %>
|
||||
<%= status_pill %>
|
||||
<%= sync_action if sync_action %>
|
||||
<summary class="min-h-15 px-4 py-3.5 cursor-pointer rounded-xl list-none [&::-webkit-details-marker]:hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<%= icon "chevron-right", size: "sm", class: "!w-3.5 !h-3.5 text-secondary group-open:rotate-90 transition-transform" %>
|
||||
<div class="flex items-center flex-wrap gap-2 min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 shrink-0 flex-1">
|
||||
<h3 class="text-sm font-medium text-primary"><%= entry[:title] %></h3>
|
||||
<%= render "settings/providers/maturity_badge", label: maturity_lbl %>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0 group-open:hidden">
|
||||
<% if meta.present? %>
|
||||
<span class="text-xs text-subdued"><%= meta %></span>
|
||||
<% end %>
|
||||
<%= status_pill %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-open:hidden">
|
||||
<%= sync_action if sync_action %>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="space-y-4 mt-4 px-4 pb-4">
|
||||
|
||||
@@ -6,11 +6,21 @@
|
||||
<%= icon "link-2", size: "sm", class: "text-secondary" %>
|
||||
</span>
|
||||
<% elsif transaction.transfer.pending? %>
|
||||
<span class="hidden lg:inline-flex items-center rounded-full bg-surface-inset px-2 py-0.5 text-xs font-medium text-secondary">
|
||||
<%= t("transactions.transfer_match.auto_matched") %>
|
||||
<span class="hidden lg:inline">
|
||||
<%= render DS::Pill.new(
|
||||
label: t("transactions.transfer_match.auto_matched"),
|
||||
tone: :neutral,
|
||||
marker: false,
|
||||
show_dot: false
|
||||
) %>
|
||||
</span>
|
||||
<span class="inline-flex lg:hidden items-center rounded-full bg-surface-inset px-2 py-0.5 text-xs font-medium text-secondary">
|
||||
<%= t("transactions.transfer_match.auto_matched_short") %>
|
||||
<span class="inline lg:hidden">
|
||||
<%= render DS::Pill.new(
|
||||
label: t("transactions.transfer_match.auto_matched_short"),
|
||||
tone: :neutral,
|
||||
marker: false,
|
||||
show_dot: false
|
||||
) %>
|
||||
</span>
|
||||
|
||||
<%= button_to transfer_path(transaction.transfer, transfer: { status: "confirmed" }),
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# Use jemalloc to reduce memory fragmentation if available
|
||||
JEMALLOC="/usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2"
|
||||
if [ -f "$JEMALLOC" ] && [ -z "${DISABLE_JEMALLOC}" ]; then
|
||||
export LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$JEMALLOC"
|
||||
else
|
||||
[ -n "${DISABLE_JEMALLOC}" ] \
|
||||
&& echo "WARNING: jemalloc disabled via DISABLE_JEMALLOC" \
|
||||
|| echo "WARNING: jemalloc not found at $JEMALLOC, skipping"
|
||||
fi
|
||||
|
||||
# If running the rails server then create or migrate existing database
|
||||
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
|
||||
./bin/rails db:prepare
|
||||
|
||||
@@ -5,16 +5,22 @@ All notable changes to the Sure Helm chart will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.7.1] - 2026-05-31]
|
||||
|
||||
### Changed
|
||||
- Bumped `pipelock.image.tag` from `2.0.0` to `2.2.0` (three minor releases behind latest). Floating `@v2` CI action pin picks up patch/minor updates automatically.
|
||||
- Refreshed pipelock feature notes in the chart README, `docs/hosting/pipelock.md`, and `pipelock.example.yaml` to reference the upstream changelog instead of a single version.
|
||||
- Bumped `pipelock.image.tag` from `2.2.0` to `2.5.0`. Picks up three releases of scanner, federation, and audit work — see the [pipelock changelog](https://github.com/luckyPipewrench/pipelock/blob/main/CHANGELOG.md) for the full surface.
|
||||
- Refreshed pipelock feature notes in the chart README, `docs/hosting/pipelock.md`, and `pipelock.example.yaml` to call out 2.5 highlights (Audit Packet v0, request-body prompt-injection blocking, SPIFFE-strict inbound envelopes, scanner attribution on MCP block receipts).
|
||||
- Expanded the `pipelock.extraConfig` escape-hatch comment to reference the new sections available in 2.5 (browser_shield, mediation_envelope, redaction, learn, media_policy, a2a_scanning, emit).
|
||||
- Defaulted `pipelock.mcpToolPolicy.enabled` to `false`. Pipelock 2.x rejects an enabled `mcp_tool_policy` with no rules, so the prior chart default (`enabled: true`, empty rules) hard-failed config validation on startup. Operators who want tool policy active must now set `enabled: true` and provide at least one entry in `pipelock.mcpToolPolicy.rules`.
|
||||
|
||||
### Added
|
||||
- `pipelock.requestBodyScanning` (pipelock 2.5+): structured config for scanning outbound request bodies for prompt-injection and bodies/sensitive headers for DLP payloads. Disabled by default to preserve existing chart behavior; opt in with `enabled: true`.
|
||||
- `pipelock.healthWatchdog` (pipelock 2.4+): structured config for the wedge-detection watchdog. Operators can opt into per-subsystem detail in `/health` responses via `exposeSubsystems: true`.
|
||||
- `pipelock.mcpToolPolicy.rules`: structured Helm values for rendering `mcp_tool_policy.rules`, including redirect-profile references.
|
||||
- `asserts.tpl` guard that fails `helm template`/`helm install` when `pipelock.mcpToolPolicy.enabled=true` is paired with an empty `rules` list, surfacing the pipelock validation error at render time instead of container startup.
|
||||
- README: CI scan status badge for the pipelock workflow.
|
||||
|
||||
## [0.6.9-alpha] - 2026-03-24
|
||||
## [0.6.9] - 2026-03-31
|
||||
|
||||
### Changed
|
||||
- Bumped `pipelock.image.tag` from `1.5.0` to `2.0.0`
|
||||
@@ -58,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
- Renamed `_asserts.tpl` to `asserts.tpl` — Helm's `_` prefix convention prevented guards from executing
|
||||
|
||||
## [0.6.7-alpha] - 2026-01-10
|
||||
## [0.6.7] - 2026-01-31
|
||||
|
||||
### Added
|
||||
- **Redis Sentinel support for Sidekiq high availability**: Application now automatically detects and configures Sidekiq to use Redis Sentinel when `redisOperator.mode=sentinel` and `redisOperator.sentinel.enabled=true`
|
||||
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: sure
|
||||
description: Official Helm chart for deploying the Sure Rails app (web + Sidekiq) on Kubernetes with optional HA PostgreSQL (CloudNativePG) and Redis.
|
||||
type: application
|
||||
version: 0.7.1-alpha.10
|
||||
appVersion: "0.7.1-alpha.10"
|
||||
version: 0.7.1-alpha.11
|
||||
appVersion: "0.7.1-alpha.11"
|
||||
|
||||
kubeVersion: ">=1.25.0-0"
|
||||
|
||||
|
||||
@@ -645,7 +645,7 @@ hpa:
|
||||
- **Forward proxy** (port 8888): Scans outbound HTTPS from Faraday-based AI clients. Auto-injected via `HTTPS_PROXY` env vars when enabled.
|
||||
- **MCP reverse proxy** (port 8889): Scans inbound MCP traffic from external AI assistants.
|
||||
|
||||
Recent pipelock releases add enhanced tool poisoning detection (full JSON schema scanning), per-read kill switch preemption, trusted domain allowlisting, MCP tool redirect profiles, signed action receipts, per-pattern DLP warn mode, and the `pipelock posture verify` / `pipelock session` CLIs. Process sandboxing and attack simulation are also available via `extraConfig` and CLI. See the [pipelock changelog](https://github.com/luckyPipewrench/pipelock/releases) for details.
|
||||
Recent pipelock releases add the Audit Packet v0 schema and language-portable verifiers (Go, TypeScript, Rust), request-body prompt-injection blocking, SPIFFE-strict inbound mediation envelopes, scanner attribution on MCP block receipts, enhanced tool poisoning detection, per-read kill switch preemption, trusted domain allowlisting, MCP tool redirect profiles, signed action receipts, per-pattern DLP warn mode, learn-and-lock behavioural contracts, the wedge-detection health watchdog, and the `pipelock posture verify` / `pipelock session` / `pipelock doctor` CLIs. Browser Shield, process sandboxing, and attack simulation are available via `extraConfig` and CLI. See the [pipelock changelog](https://github.com/luckyPipewrench/pipelock/releases) for details.
|
||||
|
||||
### Enabling Pipelock
|
||||
|
||||
@@ -653,7 +653,7 @@ Recent pipelock releases add enhanced tool poisoning detection (full JSON schema
|
||||
pipelock:
|
||||
enabled: true
|
||||
image:
|
||||
tag: "2.2.0"
|
||||
tag: "2.5.0"
|
||||
mode: balanced # strict, balanced, or audit
|
||||
```
|
||||
|
||||
@@ -677,12 +677,45 @@ pipelock:
|
||||
mcpToolPolicy:
|
||||
enabled: true
|
||||
action: redirect # or use per-rule action overrides
|
||||
rules:
|
||||
- name: redirect-fetch
|
||||
toolPattern: "^(fetch|web_fetch)$"
|
||||
action: redirect
|
||||
redirectProfile: safe-fetch
|
||||
redirectProfiles:
|
||||
safe-fetch:
|
||||
exec: ["/pipelock", "internal-redirect", "fetch-proxy"]
|
||||
reason: "Route fetch calls through audited proxy"
|
||||
```
|
||||
|
||||
### Request body scanning (pipelock 2.5+)
|
||||
|
||||
Pipelock 2.5 added prompt-injection detection on outbound request bodies (JSON, form-encoded, raw text, WebSocket frames). When enabled, findings hard-block non-provider destinations even when `action: warn`; trusted provider hosts (OpenAI, Anthropic, etc.) remain exempt through the response-scanning exemption list.
|
||||
|
||||
```yaml
|
||||
pipelock:
|
||||
requestBodyScanning:
|
||||
enabled: true
|
||||
action: warn # warn or block
|
||||
maxBodyBytes: 5242880 # 5 MB; fail-closed above this
|
||||
scanHeaders: true
|
||||
headerMode: sensitive # "sensitive" or "all"
|
||||
```
|
||||
|
||||
Disabled by default. Roll out with `action: warn` first to observe findings in logs without blocking, then flip to `action: block` once the false-positive rate is acceptable.
|
||||
|
||||
### Health watchdog
|
||||
|
||||
The wedge-detection watchdog returns 503 on `/health` when a subsystem heartbeat (proxy hot path, MCP listener, rules-engine reload watcher) goes stale. Enabled by default in pipelock; the chart exposes the controls so operators can opt into per-subsystem detail in the health payload:
|
||||
|
||||
```yaml
|
||||
pipelock:
|
||||
healthWatchdog:
|
||||
enabled: true
|
||||
intervalSeconds: 2
|
||||
exposeSubsystems: true # adds per-subsystem boolean map to /health
|
||||
```
|
||||
|
||||
### Validating your config
|
||||
|
||||
Pipelock includes CLI tools for config validation:
|
||||
@@ -693,6 +726,9 @@ pipelock simulate --config pipelock.yaml
|
||||
|
||||
# Score your config's security posture (0-100)
|
||||
pipelock audit score --config pipelock.yaml
|
||||
|
||||
# Report whether configured protections are actually enforceable
|
||||
pipelock doctor
|
||||
```
|
||||
|
||||
### Exposing MCP to external AI assistants
|
||||
@@ -811,7 +847,7 @@ See `values.yaml` for the complete configuration surface, including:
|
||||
- `migrations.*`: strategy job or initContainer
|
||||
- `simplefin.encryption.*`: enable + backfill options
|
||||
- `cronjobs.*`: custom CronJobs
|
||||
- `pipelock.*`: AI agent security proxy (forward proxy, MCP reverse proxy, DLP, injection scanning, trusted domains, tool redirect profiles, logging, serviceMonitor, ingress, PDB, extraConfig)
|
||||
- `pipelock.*`: AI agent security proxy (forward proxy, MCP reverse proxy, DLP, injection scanning, request-body scanning, health watchdog, trusted domains, tool redirect profiles, logging, serviceMonitor, ingress, PDB, extraConfig)
|
||||
- `service.*`, `ingress.*`, `serviceMonitor.*`, `hpa.*`
|
||||
|
||||
## Helm tests
|
||||
|
||||
@@ -21,3 +21,18 @@ Mutual exclusivity and configuration guards
|
||||
{{- if and $extEnabled (not $plEnabled) $requirePL -}}
|
||||
{{- fail "pipelock.requireForExternalAssistant is true but pipelock.enabled is false. Enable pipelock (pipelock.enabled=true) when using rails.externalAssistant, or set pipelock.requireForExternalAssistant=false." -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Pipelock 2.x rejects an enabled mcp_tool_policy with no rules; surface this
|
||||
at helm template time instead of waiting for the container to crash-loop.
|
||||
*/}}
|
||||
{{- if $plEnabled -}}
|
||||
{{- $mtp := .Values.pipelock.mcpToolPolicy | default (dict) -}}
|
||||
{{- if hasKey $mtp "enabled" -}}
|
||||
{{- if $mtp.enabled -}}
|
||||
{{- if eq (len ($mtp.rules | default (list))) 0 -}}
|
||||
{{- fail "pipelock.mcpToolPolicy.enabled=true requires at least one entry in pipelock.mcpToolPolicy.rules. Pipelock rejects an enabled tool policy with no rules." -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -65,6 +65,34 @@
|
||||
{{- $chainWindow = int (.Values.pipelock.toolChainDetection.windowSize | default 20) -}}
|
||||
{{- $chainGap = int (.Values.pipelock.toolChainDetection.maxGap | default 3) -}}
|
||||
{{- end -}}
|
||||
{{- $rbsEnabled := false -}}
|
||||
{{- $rbsAction := "warn" -}}
|
||||
{{- $rbsMaxBytes := 5242880 -}}
|
||||
{{- $rbsScanHeaders := true -}}
|
||||
{{- $rbsHeaderMode := "sensitive" -}}
|
||||
{{- if .Values.pipelock.requestBodyScanning -}}
|
||||
{{- if hasKey .Values.pipelock.requestBodyScanning "enabled" -}}
|
||||
{{- $rbsEnabled = .Values.pipelock.requestBodyScanning.enabled -}}
|
||||
{{- end -}}
|
||||
{{- $rbsAction = .Values.pipelock.requestBodyScanning.action | default "warn" -}}
|
||||
{{- $rbsMaxBytes = int (.Values.pipelock.requestBodyScanning.maxBodyBytes | default 5242880) -}}
|
||||
{{- if hasKey .Values.pipelock.requestBodyScanning "scanHeaders" -}}
|
||||
{{- $rbsScanHeaders = .Values.pipelock.requestBodyScanning.scanHeaders -}}
|
||||
{{- end -}}
|
||||
{{- $rbsHeaderMode = .Values.pipelock.requestBodyScanning.headerMode | default "sensitive" -}}
|
||||
{{- end -}}
|
||||
{{- $hwEnabled := true -}}
|
||||
{{- $hwInterval := 2 -}}
|
||||
{{- $hwExpose := false -}}
|
||||
{{- if .Values.pipelock.healthWatchdog -}}
|
||||
{{- if hasKey .Values.pipelock.healthWatchdog "enabled" -}}
|
||||
{{- $hwEnabled = .Values.pipelock.healthWatchdog.enabled -}}
|
||||
{{- end -}}
|
||||
{{- $hwInterval = int (.Values.pipelock.healthWatchdog.intervalSeconds | default 2) -}}
|
||||
{{- if hasKey .Values.pipelock.healthWatchdog "exposeSubsystems" -}}
|
||||
{{- $hwExpose = .Values.pipelock.healthWatchdog.exposeSubsystems -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $logFormat := "json" -}}
|
||||
{{- $logOutput := "stdout" -}}
|
||||
{{- $logIncludeAllowed := false -}}
|
||||
@@ -128,6 +156,25 @@ data:
|
||||
mcp_tool_policy:
|
||||
enabled: {{ $mcpPolicyEnabled }}
|
||||
action: {{ $mcpPolicyAction }}
|
||||
{{- if and .Values.pipelock.mcpToolPolicy .Values.pipelock.mcpToolPolicy.rules }}
|
||||
rules:
|
||||
{{- range .Values.pipelock.mcpToolPolicy.rules }}
|
||||
- name: {{ required "pipelock.mcpToolPolicy.rules[].name is required" .name | quote }}
|
||||
tool_pattern: {{ required "pipelock.mcpToolPolicy.rules[].toolPattern is required" .toolPattern | quote }}
|
||||
{{- if .argPattern }}
|
||||
arg_pattern: {{ .argPattern | quote }}
|
||||
{{- end }}
|
||||
{{- if .argKey }}
|
||||
arg_key: {{ .argKey | quote }}
|
||||
{{- end }}
|
||||
{{- if .action }}
|
||||
action: {{ .action | quote }}
|
||||
{{- end }}
|
||||
{{- if .redirectProfile }}
|
||||
redirect_profile: {{ .redirectProfile | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and .Values.pipelock.mcpToolPolicy .Values.pipelock.mcpToolPolicy.redirectProfiles }}
|
||||
redirect_profiles:
|
||||
{{- toYaml .Values.pipelock.mcpToolPolicy.redirectProfiles | nindent 8 }}
|
||||
@@ -140,6 +187,16 @@ data:
|
||||
action: {{ $chainAction }}
|
||||
window_size: {{ $chainWindow }}
|
||||
max_gap: {{ $chainGap }}
|
||||
request_body_scanning:
|
||||
enabled: {{ $rbsEnabled }}
|
||||
action: {{ $rbsAction }}
|
||||
max_body_bytes: {{ $rbsMaxBytes }}
|
||||
scan_headers: {{ $rbsScanHeaders }}
|
||||
header_mode: {{ $rbsHeaderMode }}
|
||||
health_watchdog:
|
||||
enabled: {{ $hwEnabled }}
|
||||
interval_seconds: {{ $hwInterval }}
|
||||
expose_subsystems: {{ $hwExpose }}
|
||||
logging:
|
||||
format: {{ $logFormat }}
|
||||
output: {{ $logOutput }}
|
||||
|
||||
@@ -497,7 +497,7 @@ pipelock:
|
||||
enabled: false
|
||||
image:
|
||||
repository: ghcr.io/luckypipewrench/pipelock
|
||||
tag: "2.2.0"
|
||||
tag: "2.5.0"
|
||||
pullPolicy: IfNotPresent
|
||||
imagePullSecrets: []
|
||||
replicas: 1
|
||||
@@ -530,8 +530,17 @@ pipelock:
|
||||
originPolicy: rewrite # rewrite, forward, or strip
|
||||
# MCP tool policy: pre-execution rules for tool calls (shell obfuscation, etc.)
|
||||
mcpToolPolicy:
|
||||
enabled: true
|
||||
# Enable only when at least one rule is configured; pipelock rejects an
|
||||
# enabled mcp_tool_policy with no rules.
|
||||
enabled: false
|
||||
action: warn
|
||||
# Example:
|
||||
# rules:
|
||||
# - name: redirect-fetch
|
||||
# toolPattern: "^(fetch|web_fetch)$"
|
||||
# action: redirect
|
||||
# redirectProfile: safe-fetch
|
||||
rules: []
|
||||
# Redirect profiles: route matched tool calls to audited handler programs instead
|
||||
# of blocking. The handler returns a synthetic MCP response. Fail-closed on error.
|
||||
# Example:
|
||||
@@ -550,6 +559,24 @@ pipelock:
|
||||
action: warn
|
||||
windowSize: 20
|
||||
maxGap: 3
|
||||
# Request body scanning (pipelock 2.5+): detect prompt-injection payloads
|
||||
# in outbound request bodies (JSON, form-encoded, raw text, WebSocket frames).
|
||||
# In enforce mode, prompt-injection findings hard-block non-provider
|
||||
# destinations even when action is "warn"; trusted provider hosts (OpenAI,
|
||||
# Anthropic, etc.) remain exempt via the response_scanning exemption list.
|
||||
requestBodyScanning:
|
||||
enabled: false
|
||||
action: warn # warn or block
|
||||
maxBodyBytes: 5242880 # 5 MB; fail-closed above this
|
||||
scanHeaders: true # scan request headers for DLP
|
||||
headerMode: sensitive # "sensitive" or "all"
|
||||
# Health watchdog (pipelock 2.4+): /health returns 503 when any subsystem
|
||||
# heartbeat goes stale. Enabled by default in pipelock; exposed here so
|
||||
# operators can opt into per-subsystem detail in the health payload.
|
||||
healthWatchdog:
|
||||
enabled: true
|
||||
intervalSeconds: 2
|
||||
exposeSubsystems: false # include per-subsystem map in /health response
|
||||
service:
|
||||
type: ClusterIP
|
||||
resources:
|
||||
@@ -601,8 +628,10 @@ pipelock:
|
||||
includeBlocked: true
|
||||
|
||||
# Escape hatch: ADDITIONAL config sections appended to pipelock.yaml.
|
||||
# Use for sections not covered by structured values above (session_profiling,
|
||||
# data_budget, adaptive_enforcement, kill_switch, sandbox, reverse_proxy, etc.)
|
||||
# Use for sections not covered by structured values above. Examples:
|
||||
# session_profiling, data_budget, adaptive_enforcement, kill_switch, sandbox,
|
||||
# reverse_proxy, redaction, browser_shield, mediation_envelope, learn,
|
||||
# media_policy, a2a_scanning, emit (OTel agent.threat.detection attributes).
|
||||
# Do NOT duplicate keys already rendered above - behavior is parser-dependent.
|
||||
extraConfig: {}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ x-rails-env: &rails_env
|
||||
|
||||
services:
|
||||
pipelock:
|
||||
image: ghcr.io/luckypipewrench/pipelock:latest # pin to a specific version (e.g., :2.2.0) for production
|
||||
image: ghcr.io/luckypipewrench/pipelock:latest # pin to a specific version (e.g., :2.5.0) for production
|
||||
container_name: pipelock
|
||||
hostname: pipelock
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -61,13 +61,13 @@ de:
|
||||
rows_label: Zeilen
|
||||
unassigned_account: Neues Konto für nicht zugewiesene Zeilen erstellen?
|
||||
show:
|
||||
account_mapping_description: Weise alle Konten aus deiner importierten Datei den bestehenden Konten in %{product} zu. Du kannst auch neue Konten hinzufügen oder sie ohne Kategorie lassen.
|
||||
account_mapping_description: Weise alle Konten aus deiner importierten Datei den bestehenden Konten in %{product_name} zu. Du kannst auch neue Konten hinzufügen oder sie ohne Kategorie lassen.
|
||||
account_mapping_title: Konten zuweisen
|
||||
account_type_mapping_description: Weise alle Kontotypen aus deiner importierten Datei den Kontotypen in %{product} zu.
|
||||
account_type_mapping_description: Weise alle Kontotypen aus deiner importierten Datei den Kontotypen in %{product_name} zu.
|
||||
account_type_mapping_title: Kontotypen zuweisen
|
||||
category_mapping_description: Weise alle Kategorien aus deiner importierten Datei den bestehenden Kategorien in %{product} zu. Du kannst auch neue Kategorien hinzufügen oder sie ohne Kategorie lassen.
|
||||
category_mapping_description: Weise alle Kategorien aus deiner importierten Datei den bestehenden Kategorien in %{product_name} zu. Du kannst auch neue Kategorien hinzufügen oder sie ohne Kategorie lassen.
|
||||
category_mapping_title: Kategorien zuweisen
|
||||
tag_mapping_description: Weise alle Tags aus deiner importierten Datei den bestehenden Tags in %{product} zu. Du kannst auch neue Tags hinzufügen oder sie ohne Kategorie lassen.
|
||||
tag_mapping_description: Weise alle Tags aus deiner importierten Datei den bestehenden Tags in %{product_name} zu. Du kannst auch neue Tags hinzufügen oder sie ohne Kategorie lassen.
|
||||
tag_mapping_title: Tags zuweisen
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -155,17 +155,17 @@ en:
|
||||
show:
|
||||
invalid_data: "You have invalid data, please edit until all errors are resolved"
|
||||
account_mapping_description: Assign all of your imported file's accounts to
|
||||
Maybe's existing accounts. You can also add new accounts or leave them
|
||||
%{product_name}'s existing accounts. You can also add new accounts or leave them
|
||||
uncategorized.
|
||||
account_mapping_title: Assign your accounts
|
||||
account_type_mapping_description: Assign all of your imported file's account
|
||||
types to Maybe's
|
||||
types to %{product_name}'s
|
||||
account_type_mapping_title: Assign your account types
|
||||
category_mapping_description: Assign all of your imported file's categories
|
||||
to Maybe's existing categories. You can also add new categories or leave
|
||||
to %{product_name}'s existing categories. You can also add new categories or leave
|
||||
them uncategorized.
|
||||
category_mapping_title: Assign your categories
|
||||
tag_mapping_description: Assign all of your imported file's tags to Maybe's
|
||||
tag_mapping_description: Assign all of your imported file's tags to %{product_name}'s
|
||||
existing tags. You can also add new tags or leave them uncategorized.
|
||||
tag_mapping_title: Assign your tags
|
||||
uploads:
|
||||
|
||||
@@ -61,13 +61,13 @@ es:
|
||||
rows_label: Filas
|
||||
unassigned_account: ¿Necesitas crear una nueva cuenta para las filas sin asignar?
|
||||
show:
|
||||
account_mapping_description: Asigna todas las cuentas de tu archivo importado a las cuentas existentes de Sure. También puedes añadir nuevas cuentas o dejarlas sin categorizar.
|
||||
account_mapping_description: Asigna todas las cuentas de tu archivo importado a las cuentas existentes de %{product_name}. También puedes añadir nuevas cuentas o dejarlas sin categorizar.
|
||||
account_mapping_title: Asigna tus cuentas
|
||||
account_type_mapping_description: Asigna todos los tipos de cuenta de tu archivo importado a los de Sure.
|
||||
account_type_mapping_description: Asigna todos los tipos de cuenta de tu archivo importado a los de %{product_name}.
|
||||
account_type_mapping_title: Asigna tus tipos de cuenta
|
||||
category_mapping_description: Asigna todas las categorías de tu archivo importado a las categorías existentes de Sure. También puedes añadir nuevas categorías o dejarlas sin categorizar.
|
||||
category_mapping_description: Asigna todas las categorías de tu archivo importado a las categorías existentes de %{product_name}. También puedes añadir nuevas categorías o dejarlas sin categorizar.
|
||||
category_mapping_title: Asigna tus categorías
|
||||
tag_mapping_description: Asigna todas las etiquetas de tu archivo importado a las etiquetas existentes de Sure. También puedes añadir nuevas etiquetas o dejarlas sin categorizar.
|
||||
tag_mapping_description: Asigna todas las etiquetas de tu archivo importado a las etiquetas existentes de %{product_name}. También puedes añadir nuevas etiquetas o dejarlas sin categorizar.
|
||||
tag_mapping_title: Asigna tus etiquetas
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -134,13 +134,13 @@ hu:
|
||||
next: Tovább
|
||||
show:
|
||||
invalid_data: "Érvénytelen adatok találhatók, kérjük szerkeszd, amíg minden hibát ki nem javítasz"
|
||||
account_mapping_description: Rendeld hozzá az importált fájl számláit a Maybe meglévő számláihoz. Új számlákat is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
account_mapping_description: Rendeld hozzá az importált fájl számláit a %{product_name} meglévő számláihoz. Új számlákat is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
account_mapping_title: Számlák hozzárendelése
|
||||
account_type_mapping_description: Rendeld hozzá az importált fájl számlatípusait a Maybe típusaihoz.
|
||||
account_type_mapping_description: Rendeld hozzá az importált fájl számlatípusait a %{product_name} típusaihoz.
|
||||
account_type_mapping_title: Számlatípusok hozzárendelése
|
||||
category_mapping_description: Rendeld hozzá az importált fájl kategóriáit a Maybe meglévő kategóriáihoz. Új kategóriákat is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
category_mapping_description: Rendeld hozzá az importált fájl kategóriáit a %{product_name} meglévő kategóriáihoz. Új kategóriákat is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
category_mapping_title: Kategóriák hozzárendelése
|
||||
tag_mapping_description: Rendeld hozzá az importált fájl címkéit a Maybe meglévő címkéihez. Új címkéket is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
tag_mapping_description: Rendeld hozzá az importált fájl címkéit a %{product_name} meglévő címkéihez. Új címkéket is hozzáadhatsz, vagy hagyhatod őket kategorizálatlanul.
|
||||
tag_mapping_title: Címkék hozzárendelése
|
||||
uploads:
|
||||
update:
|
||||
|
||||
@@ -31,18 +31,18 @@ nb:
|
||||
unassigned_account: Trenger å opprette en ny konto for utilordnede rader?
|
||||
show:
|
||||
account_mapping_description: Tilordne alle kontoene i den importerte filen din til
|
||||
Maybes eksisterende kontoer. Du kan også legge til nye kontoer eller la dem
|
||||
%{product_name}s eksisterende kontoer. Du kan også legge til nye kontoer eller la dem
|
||||
være ukategorisert.
|
||||
account_mapping_title: Tilordne kontoene dine
|
||||
account_type_mapping_description: Tilordne alle kontotypene i den importerte filen din til
|
||||
Maybes
|
||||
%{product_name}s
|
||||
account_type_mapping_title: Tilordne kontotypene dine
|
||||
category_mapping_description: Tilordne alle kategoriene i den importerte filen din til
|
||||
Maybes eksisterende kategorier. Du kan også legge til nye kategorier eller la dem
|
||||
%{product_name}s eksisterende kategorier. Du kan også legge til nye kategorier eller la dem
|
||||
være ukategorisert.
|
||||
category_mapping_title: Tilordne kategoriene dine
|
||||
tag_mapping_description: Tilordne alle tagene i den importerte filen din til
|
||||
Maybes eksisterende tagger. Du kan også legge til nye tagger eller la dem
|
||||
%{product_name}s eksisterende tagger. Du kan også legge til nye tagger eller la dem
|
||||
være ukategorisert.
|
||||
tag_mapping_title: Tilordne tagene dine
|
||||
uploads:
|
||||
|
||||
@@ -37,13 +37,13 @@ nl:
|
||||
rows_label: Rijen
|
||||
unassigned_account: Moet u een nieuw account aanmaken voor niet-toegewezen rijen?
|
||||
show:
|
||||
account_mapping_description: Wijs alle accounts van uw geïmporteerde bestand toe aan bestaande %{product} accounts. U kunt ook nieuwe accounts toevoegen of ze ongecategoriseerd laten.
|
||||
account_mapping_description: Wijs alle accounts van uw geïmporteerde bestand toe aan bestaande %{product_name} accounts. U kunt ook nieuwe accounts toevoegen of ze ongecategoriseerd laten.
|
||||
account_mapping_title: Uw accounts toewijzen
|
||||
account_type_mapping_description: Wijs alle accounttypen van uw geïmporteerde bestand toe aan %{product} accounttypen
|
||||
account_type_mapping_description: Wijs alle accounttypen van uw geïmporteerde bestand toe aan %{product_name} accounttypen
|
||||
account_type_mapping_title: Uw accounttypen toewijzen
|
||||
category_mapping_description: Wijs alle categorieën van uw geïmporteerde bestand toe aan bestaande %{product} categorieën. U kunt ook nieuwe categorieën toevoegen of ze ongecategoriseerd laten.
|
||||
category_mapping_description: Wijs alle categorieën van uw geïmporteerde bestand toe aan bestaande %{product_name} categorieën. U kunt ook nieuwe categorieën toevoegen of ze ongecategoriseerd laten.
|
||||
category_mapping_title: Uw categorieën toewijzen
|
||||
tag_mapping_description: Wijs alle tags van uw geïmporteerde bestand toe aan bestaande %{product} tags. U kunt ook nieuwe tags toevoegen of ze ongecategoriseerd laten.
|
||||
tag_mapping_description: Wijs alle tags van uw geïmporteerde bestand toe aan bestaande %{product_name} tags. U kunt ook nieuwe tags toevoegen of ze ongecategoriseerd laten.
|
||||
tag_mapping_title: Uw tags toewijzen
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -74,13 +74,13 @@ pl:
|
||||
rows_label: Wiersze
|
||||
unassigned_account: Chcesz utworzyć nowe konto dla nieprzypisanych wierszy?
|
||||
show:
|
||||
account_mapping_description: Przypisz wszystkie konta z importowanego pliku do istniejących kont %{product}. Możesz też dodać nowe konta lub zostawić je bez przypisania.
|
||||
account_mapping_description: Przypisz wszystkie konta z importowanego pliku do istniejących kont %{product_name}. Możesz też dodać nowe konta lub zostawić je bez przypisania.
|
||||
account_mapping_title: Przypisz konta
|
||||
account_type_mapping_description: Przypisz wszystkie typy kont z importowanego pliku do typów używanych w %{product}.
|
||||
account_type_mapping_description: Przypisz wszystkie typy kont z importowanego pliku do typów używanych w %{product_name}.
|
||||
account_type_mapping_title: Przypisz typy kont
|
||||
category_mapping_description: Przypisz wszystkie kategorie z importowanego pliku do istniejących kategorii %{product}. Możesz też dodać nowe kategorie lub zostawić je bez przypisania.
|
||||
category_mapping_description: Przypisz wszystkie kategorie z importowanego pliku do istniejących kategorii %{product_name}. Możesz też dodać nowe kategorie lub zostawić je bez przypisania.
|
||||
category_mapping_title: Przypisz kategorie
|
||||
tag_mapping_description: Przypisz wszystkie tagi z importowanego pliku do istniejących tagów %{product}. Możesz też dodać nowe tagi lub zostawić je bez przypisania.
|
||||
tag_mapping_description: Przypisz wszystkie tagi z importowanego pliku do istniejących tagów %{product_name}. Możesz też dodać nowe tagi lub zostawić je bez przypisania.
|
||||
tag_mapping_title: Przypisz tagi
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -36,18 +36,18 @@ pt-BR:
|
||||
unassigned_account: Precisa criar uma nova conta para linhas não atribuídas?
|
||||
show:
|
||||
account_mapping_description: Atribua todas as contas do seu arquivo importado às
|
||||
contas existentes no %{product}. Você também pode adicionar novas contas ou deixá-las
|
||||
contas existentes no %{product_name}. Você também pode adicionar novas contas ou deixá-las
|
||||
sem categoria.
|
||||
account_mapping_title: Atribuir suas contas
|
||||
account_type_mapping_description: Atribuir todos os tipos de conta do seu arquivo importado aos
|
||||
do %{product}
|
||||
do %{product_name}
|
||||
account_type_mapping_title: Atribuir seus tipos de conta
|
||||
category_mapping_description: Atribua todas as categorias do seu arquivo importado às
|
||||
categorias existentes no %{product}. Você também pode adicionar novas categorias ou deixá-las
|
||||
categorias existentes no %{product_name}. Você também pode adicionar novas categorias ou deixá-las
|
||||
sem categoria.
|
||||
category_mapping_title: Atribuir suas categorias
|
||||
tag_mapping_description: Atribua todas as tags do seu arquivo importado às
|
||||
tags existentes no %{product}. Você também pode adicionar novas tags ou deixá-las
|
||||
tags existentes no %{product_name}. Você também pode adicionar novas tags ou deixá-las
|
||||
sem categoria.
|
||||
tag_mapping_title: Atribuir suas tags
|
||||
uploads:
|
||||
|
||||
@@ -26,13 +26,13 @@ ro:
|
||||
rows_label: Rânduri
|
||||
unassigned_account: Trebuie să creezi un cont nou pentru rândurile neasignate?
|
||||
show:
|
||||
account_mapping_description: Asignează toate conturile din fișierul tău importat conturilor existente din Maybe. Poți adăuga și conturi noi sau le poți lăsa necategorizate.
|
||||
account_mapping_description: Asignează toate conturile din fișierul tău importat conturilor existente din %{product_name}. Poți adăuga și conturi noi sau le poți lăsa necategorizate.
|
||||
account_mapping_title: Asignează-ți conturile
|
||||
account_type_mapping_description: Asignează toate tipurile de cont din fișierul tău importat celor din Maybe.
|
||||
account_type_mapping_description: Asignează toate tipurile de cont din fișierul tău importat celor din %{product_name}.
|
||||
account_type_mapping_title: Asignează-ți tipurile de cont
|
||||
category_mapping_description: Asignează toate categoriile din fișierul tău importat categoriilor existente din Maybe. Poți adăuga și categorii noi sau le poți lăsa necategorizate.
|
||||
category_mapping_description: Asignează toate categoriile din fișierul tău importat categoriilor existente din %{product_name}. Poți adăuga și categorii noi sau le poți lăsa necategorizate.
|
||||
category_mapping_title: Asignează-ți categoriile
|
||||
tag_mapping_description: Asignează toate etichetele din fișierul tău importat etichetelor existente din Maybe. Poți adăuga și etichete noi sau le poți lăsa necategorizate.
|
||||
tag_mapping_description: Asignează toate etichetele din fișierul tău importat etichetelor existente din %{product_name}. Poți adăuga și etichete noi sau le poți lăsa necategorizate.
|
||||
tag_mapping_title: Asignează-ți etichetele
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -26,13 +26,13 @@ tr:
|
||||
rows_label: Satırlar
|
||||
unassigned_account: Atanmamış satırlar için yeni bir hesap oluşturmak ister misiniz?
|
||||
show:
|
||||
account_mapping_description: "İçe aktardığınız dosyadaki tüm hesapları %{product}'deki mevcut hesaplara eşleyin. Ayrıca yeni hesaplar ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
account_mapping_description: "İçe aktardığınız dosyadaki tüm hesapları %{product_name}'deki mevcut hesaplara eşleyin. Ayrıca yeni hesaplar ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
account_mapping_title: Hesaplarınızı eşleyin
|
||||
account_type_mapping_description: "İçe aktardığınız dosyadaki tüm hesap türlerini %{product}'deki hesap türlerine eşleyin."
|
||||
account_type_mapping_description: "İçe aktardığınız dosyadaki tüm hesap türlerini %{product_name}'deki hesap türlerine eşleyin."
|
||||
account_type_mapping_title: Hesap türlerinizi eşleyin
|
||||
category_mapping_description: "İçe aktardığınız dosyadaki tüm kategorileri %{product}'deki mevcut kategorilere eşleyin. Ayrıca yeni kategoriler ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
category_mapping_description: "İçe aktardığınız dosyadaki tüm kategorileri %{product_name}'deki mevcut kategorilere eşleyin. Ayrıca yeni kategoriler ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
category_mapping_title: Kategorilerinizi eşleyin
|
||||
tag_mapping_description: "İçe aktardığınız dosyadaki tüm etiketleri %{product}'deki mevcut etiketlere eşleyin. Ayrıca yeni etiketler ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
tag_mapping_description: "İçe aktardığınız dosyadaki tüm etiketleri %{product_name}'deki mevcut etiketlere eşleyin. Ayrıca yeni etiketler ekleyebilir veya kategorize etmeden bırakabilirsiniz."
|
||||
tag_mapping_title: Etiketlerinizi eşleyin
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -34,13 +34,13 @@ zh-CN:
|
||||
sure_mapping_label: "%{product_name} 中的 %{mapping}"
|
||||
unassigned_account: 需要为未分配行创建新账户?
|
||||
show:
|
||||
account_mapping_description: 将导入文件中的所有账户分配到 %{product} 的现有账户。您也可以新建账户或保留为未分类。
|
||||
account_mapping_description: 将导入文件中的所有账户分配到 %{product_name} 的现有账户。您也可以新建账户或保留为未分类。
|
||||
account_mapping_title: 分配账户
|
||||
account_type_mapping_description: 将导入文件中的所有账户类型分配到 %{product} 的账户类型
|
||||
account_type_mapping_description: 将导入文件中的所有账户类型分配到 %{product_name} 的账户类型
|
||||
account_type_mapping_title: 分配账户类型
|
||||
category_mapping_description: 将导入文件中的所有分类分配到 %{product} 的现有分类。您也可以新建分类或保留为未分类。
|
||||
category_mapping_description: 将导入文件中的所有分类分配到 %{product_name} 的现有分类。您也可以新建分类或保留为未分类。
|
||||
category_mapping_title: 分配分类
|
||||
tag_mapping_description: 将导入文件中的所有标签分配到 %{product} 的现有标签。您也可以新建标签或保留为未分类。
|
||||
tag_mapping_description: 将导入文件中的所有标签分配到 %{product_name} 的现有标签。您也可以新建标签或保留为未分类。
|
||||
tag_mapping_title: 分配标签
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -34,13 +34,13 @@ zh-TW:
|
||||
rows_label: 資料列
|
||||
unassigned_account: 需要為未分配的資料列建立新帳戶嗎?
|
||||
show:
|
||||
account_mapping_description: 將匯入檔案中的帳戶對應到 Maybe 現有的帳戶。您也可以新增帳戶,或暫時不進行分類。
|
||||
account_mapping_description: 將匯入檔案中的帳戶對應到 %{product_name} 現有的帳戶。您也可以新增帳戶,或暫時不進行分類。
|
||||
account_mapping_title: 分配您的帳戶
|
||||
account_type_mapping_description: 將匯入檔案中的帳戶類型對應到 Maybe 的帳戶類型
|
||||
account_type_mapping_description: 將匯入檔案中的帳戶類型對應到 %{product_name} 的帳戶類型
|
||||
account_type_mapping_title: 分配您的帳戶類型
|
||||
category_mapping_description: 將匯入檔案中的類別對應到 Maybe 現有的類別。您也可以新增類別,或暫時不進行分類。
|
||||
category_mapping_description: 將匯入檔案中的類別對應到 %{product_name} 現有的類別。您也可以新增類別,或暫時不進行分類。
|
||||
category_mapping_title: 分配您的類別
|
||||
tag_mapping_description: 將匯入檔案中的標籤對應到 Maybe 現有的標籤。您也可以新增標籤,或暫時不進行分類。
|
||||
tag_mapping_description: 將匯入檔案中的標籤對應到 %{product_name} 現有的標籤。您也可以新增標籤,或暫時不進行分類。
|
||||
tag_mapping_title: 分配您的標籤
|
||||
uploads:
|
||||
show:
|
||||
|
||||
@@ -37,7 +37,7 @@ es:
|
||||
never_expires: "Nunca expira"
|
||||
permissions: "Permisos"
|
||||
usage_instructions_title: "Cómo usar tu clave API"
|
||||
usage_instructions: "Incluye tu clave API en el encabezado X-Api-Key al realizar solicitudes a la API de Maybe:"
|
||||
usage_instructions: "Incluye tu clave API en el encabezado X-Api-Key al realizar solicitudes a la API de %{product_name}:"
|
||||
regenerate_key: "Crear Nueva Clave"
|
||||
revoke_key: "Revocar Clave"
|
||||
revoke_confirmation: "¿Estás seguro de que deseas revocar esta clave API? Esta acción no se puede deshacer y deshabilitará inmediatamente todas las aplicaciones que usen esta clave."
|
||||
|
||||
@@ -36,7 +36,7 @@ nb:
|
||||
never_expires: "Utløper aldri"
|
||||
permissions: "Tillatelser"
|
||||
usage_instructions_title: "Hvordan bruke din API-nøkkel"
|
||||
usage_instructions: "Inkluder din API-nøkkel i X-Api-Key-headeren når du gjør forespørsler til Maybe API-et:"
|
||||
usage_instructions: "Inkluder din API-nøkkel i X-Api-Key-headeren når du gjør forespørsler til %{product_name} API-et:"
|
||||
regenerate_key: "Opprett ny nøkkel"
|
||||
revoke_key: "Tilbakekall nøkkel"
|
||||
revoke_confirmation: "Er du sikker på at du vil tilbakekalle denne API-nøkkelen? Denne handlingen kan ikke angres og vil umiddelbart deaktivere alle applikasjoner som bruker denne nøkkelen."
|
||||
|
||||
@@ -37,7 +37,7 @@ pl:
|
||||
never_expires: Nigdy nie wygasa
|
||||
permissions: Uprawnienia
|
||||
usage_instructions_title: Jak używać klucza API
|
||||
usage_instructions: 'Dołącz klucz API w nagłówku X-Api-Key podczas wysyłania żądań do API Maybe:'
|
||||
usage_instructions: 'Dołącz klucz API w nagłówku X-Api-Key podczas wysyłania żądań do API %{product_name}:'
|
||||
regenerate_key: Utwórz nowy klucz
|
||||
revoke_key: Unieważnij klucz
|
||||
revoke_confirmation: Czy na pewno chcesz unieważnić ten klucz API? Tej akcji nie można cofnąć, a wszystkie aplikacje używające tego klucza zostaną natychmiast wyłączone.
|
||||
|
||||
@@ -36,7 +36,7 @@ tr:
|
||||
never_expires: "Süresiz"
|
||||
permissions: "Yetkiler"
|
||||
usage_instructions_title: "API anahtarınızı nasıl kullanırsınız"
|
||||
usage_instructions: "Maybe API'ye istek yaparken API anahtarınızı X-Api-Key başlığına ekleyin:"
|
||||
usage_instructions: "%{product_name} API'ye istek yaparken API anahtarınızı X-Api-Key başlığına ekleyin:"
|
||||
regenerate_key: "Yeni Anahtar Oluştur"
|
||||
revoke_key: "Anahtarı İptal Et"
|
||||
revoke_confirmation: "Bu API anahtarını iptal etmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve bu anahtarı kullanan tüm uygulamalar hemen devre dışı kalır."
|
||||
|
||||
@@ -37,7 +37,7 @@ zh-TW:
|
||||
never_expires: "永不過期"
|
||||
permissions: "權限範圍"
|
||||
usage_instructions_title: "如何使用您的 API 金鑰"
|
||||
usage_instructions: "在向 Maybe API 發送請求時,請在 X-Api-Key 標頭 (Header) 中包含您的 API 金鑰:"
|
||||
usage_instructions: "在向 %{product_name} API 發送請求時,請在 X-Api-Key 標頭 (Header) 中包含您的 API 金鑰:"
|
||||
regenerate_key: "建立新金鑰"
|
||||
revoke_key: "撤銷金鑰"
|
||||
revoke_confirmation: "您確定要撤銷此 API 金鑰嗎?此操作無法還原,且會立即停用所有使用此金鑰的應用程式。"
|
||||
|
||||
@@ -450,13 +450,18 @@ Pipelock scans for prompt injection, DLP violations, and tool poisoning. The ext
|
||||
|
||||
**`NO_PROXY` behavior (Helm/Kubernetes only):** The Helm chart's env template sets `NO_PROXY` to include `.svc.cluster.local` and other internal domains. This means in-cluster agent URLs (like `http://agent.namespace.svc.cluster.local:18789`) bypass the forward proxy and go directly. If your agent is in-cluster, its traffic won't be forward-proxy scanned (but MCP callbacks from the agent are still scanned by the reverse proxy). Docker Compose deployments use a different `NO_PROXY` set; check your compose file for the exact values.
|
||||
|
||||
**`mcpToolPolicy` note:** The Helm chart's `pipelock.mcpToolPolicy.enabled` defaults to `true`. If you haven't defined any policy rules, disable it:
|
||||
**`mcpToolPolicy` note:** The Helm chart's `pipelock.mcpToolPolicy.enabled` defaults to `false`. Pipelock rejects an enabled tool policy with no rules, so the chart ships it off by default. To turn it on, define at least one rule and set `enabled: true`:
|
||||
|
||||
```yaml
|
||||
# Helm values
|
||||
pipelock:
|
||||
mcpToolPolicy:
|
||||
enabled: false
|
||||
enabled: true
|
||||
action: warn
|
||||
rules:
|
||||
- name: example
|
||||
toolPattern: "^shell$"
|
||||
action: block
|
||||
```
|
||||
|
||||
See the [Pipelock documentation](https://github.com/luckyPipewrench/pipelock) for tool policy configuration details.
|
||||
|
||||
@@ -231,7 +231,7 @@ Pipelock provides:
|
||||
- **Tool poisoning detection**: Prevents malicious tool call sequences
|
||||
- **Policy enforcement**: Block or warn on suspicious patterns
|
||||
|
||||
See the [Pipelock documentation](pipelock.md) and the example configuration in `compose.example.pipelock.yml` for setup instructions.
|
||||
See the [Pipelock documentation](pipelock.md) and the example configuration in `compose.example.ai.yml` for setup instructions.
|
||||
|
||||
### Network Security
|
||||
|
||||
|
||||
@@ -77,13 +77,13 @@ Enable Pipelock in your Helm values:
|
||||
pipelock:
|
||||
enabled: true
|
||||
image:
|
||||
tag: "2.2.0"
|
||||
tag: "2.5.0"
|
||||
mode: balanced
|
||||
```
|
||||
|
||||
This creates a separate Deployment, Service, and ConfigMap. The chart auto-injects `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` into web and worker pods.
|
||||
|
||||
Recent pipelock releases add trusted domain allowlisting, MCP tool redirect profiles, enhanced tool poisoning detection (full JSON schema scanning), per-read kill switch preemption, signed action receipts, per-pattern DLP warn mode, and the `pipelock posture verify` / `pipelock session` CLI commands. See the [pipelock changelog](https://github.com/luckyPipewrench/pipelock/releases) for details.
|
||||
Recent pipelock releases add the Audit Packet v0 schema and language-portable verifiers (Go/TypeScript/Rust), request-body prompt-injection blocking, SPIFFE-strict inbound mediation envelopes, scanner attribution on MCP block receipts, trusted domain allowlisting, MCP tool redirect profiles, enhanced tool poisoning detection, per-read kill switch preemption, signed action receipts, per-pattern DLP warn mode, learn-and-lock behavioural contracts, the wedge-detection health watchdog, and the `pipelock posture verify` / `pipelock session` / `pipelock doctor` CLI commands. See the [pipelock changelog](https://github.com/luckyPipewrench/pipelock/releases) for details.
|
||||
|
||||
### Exposing MCP to external agents (Kubernetes)
|
||||
|
||||
@@ -149,6 +149,7 @@ The `pipelock.example.yaml` file (Docker Compose) or ConfigMap (Helm) controls s
|
||||
| `trusted_domains` | Allow internal services whose public DNS resolves to private IPs |
|
||||
| `forward_proxy` | Outbound HTTPS scanning (tunnel timeouts, idle timeouts) |
|
||||
| `dlp` | Data loss prevention (scan env vars, built-in patterns) |
|
||||
| `request_body_scanning` | Scan outbound request bodies for prompt-injection and bodies/sensitive headers for DLP (pipelock 2.5+) |
|
||||
| `response_scanning` | Scan LLM responses for prompt injection |
|
||||
| `mcp_input_scanning` | Scan inbound MCP requests |
|
||||
| `mcp_tool_scanning` | Validate tool calls, detect drift |
|
||||
@@ -156,6 +157,7 @@ The `pipelock.example.yaml` file (Docker Compose) or ConfigMap (Helm) controls s
|
||||
| `mcp_session_binding` | Pin tool inventory, detect manipulation |
|
||||
| `tool_chain_detection` | Multi-step attack patterns |
|
||||
| `websocket_proxy` | WebSocket frame scanning (disabled by default) |
|
||||
| `health_watchdog` | Wedge-detection on subsystem heartbeats, returns 503 on stall (pipelock 2.4+) |
|
||||
| `logging` | Output format (json/text), verbosity |
|
||||
|
||||
For the Helm chart, most sections are configurable via `values.yaml`. For additional sections not covered by structured values (session profiling, data budgets, kill switch, sandbox, reverse proxy, adaptive enforcement), use the `extraConfig` escape hatch:
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# Pipelock configuration for Docker Compose
|
||||
# See https://github.com/luckyPipewrench/pipelock for full options.
|
||||
#
|
||||
# Recent additions: trusted_domains, redirect profiles, attack simulation,
|
||||
# security scoring, process sandbox, enhanced tool poisoning detection, signed
|
||||
# action receipts, per-pattern DLP warn mode, and the `pipelock posture verify`
|
||||
# / `pipelock session` CLIs.
|
||||
# Recent additions (2.5): Audit Packet v0 schema with Go/TypeScript/Rust
|
||||
# verifiers, request-body prompt-injection blocking, SPIFFE-strict inbound
|
||||
# mediation envelopes, scanner attribution on MCP block receipts, wedge-
|
||||
# detection health watchdog, learn-and-lock behavioural contracts, trusted
|
||||
# domains, redirect profiles, attack simulation, security scoring, process
|
||||
# sandbox, signed action receipts, per-pattern DLP warn mode, and the
|
||||
# `pipelock posture verify` / `pipelock session` / `pipelock doctor` CLIs.
|
||||
# Run `pipelock simulate --config <file>` to test your config against 24 attack scenarios.
|
||||
# Run `pipelock audit score --config <file>` for a security posture score (0-100).
|
||||
# Run `pipelock doctor` to verify configured protections are actually enforceable.
|
||||
|
||||
version: 1
|
||||
mode: balanced
|
||||
@@ -72,3 +76,23 @@ tool_chain_detection:
|
||||
action: warn
|
||||
window_size: 20
|
||||
max_gap: 3
|
||||
|
||||
# Request body scanning (pipelock 2.5+): detect prompt-injection payloads in
|
||||
# outbound request bodies (JSON, form-encoded, raw text, WebSocket frames).
|
||||
# In enforce mode, prompt-injection findings hard-block non-provider
|
||||
# destinations even when action is "warn". Trusted provider hosts (OpenAI,
|
||||
# Anthropic, etc.) remain exempt via the response_scanning exemption list.
|
||||
request_body_scanning:
|
||||
enabled: false
|
||||
action: warn
|
||||
max_body_bytes: 5242880
|
||||
scan_headers: true
|
||||
header_mode: sensitive
|
||||
|
||||
# Health watchdog (pipelock 2.4+): /health returns 503 when any subsystem
|
||||
# heartbeat goes stale. Enabled by default; set expose_subsystems true to
|
||||
# include a per-subsystem boolean map in /health responses.
|
||||
health_watchdog:
|
||||
enabled: true
|
||||
interval_seconds: 2
|
||||
expose_subsystems: false
|
||||
|
||||
42
test/controllers/enable_banking_items_controller_test.rb
Normal file
42
test/controllers/enable_banking_items_controller_test.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "openssl"
|
||||
|
||||
class EnableBankingItemsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
@family = families(:dylan_family)
|
||||
@item = @family.enable_banking_items.create!(
|
||||
name: "Test Connection",
|
||||
country_code: "DE",
|
||||
application_id: "test_app_id",
|
||||
client_certificate: OpenSSL::PKey::RSA.new(2048).to_pem
|
||||
)
|
||||
end
|
||||
|
||||
test "select_bank exposes ASPSP BIC in the searchable data attribute" do
|
||||
Provider::EnableBanking.any_instance.stubs(:get_aspsps).returns(
|
||||
aspsps: [
|
||||
{
|
||||
name: "ING-DiBa AG",
|
||||
country: "DE",
|
||||
bic: "INGDDEFF",
|
||||
beta: false,
|
||||
psu_types: [ "personal" ],
|
||||
auth_methods: [ { approach: "REDIRECT" } ]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
get select_bank_enable_banking_item_url(@item)
|
||||
|
||||
assert_response :success
|
||||
haystack = @response.body[/data-bank-search="([^"]*)"/, 1]
|
||||
assert haystack, "Expected list items to render a data-bank-search attribute the client filter reads from"
|
||||
assert_includes haystack, "ingddeff",
|
||||
"Expected the searchable data attribute to include the BIC so users can find banks by BIC code"
|
||||
assert_includes haystack, "ing-diba ag",
|
||||
"Expected the searchable data attribute to still include the bank name (existing name-search behavior)"
|
||||
end
|
||||
end
|
||||
@@ -95,6 +95,55 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
# Regression: issue #1824. The OpenAI form auto-submits on blur, so entering
|
||||
# the URI base before the model fires a partial submit that fails validation.
|
||||
# The re-rendered form must show the user's submitted URI base — not the
|
||||
# still-blank saved value — so they can finish typing the model.
|
||||
test "preserves submitted openai uri base in form when validation fails" do
|
||||
with_self_hosting do
|
||||
Setting.openai_uri_base = nil
|
||||
Setting.openai_model = ""
|
||||
|
||||
patch settings_hosting_url, params: { setting: { openai_uri_base: "https://api.example.com/v1" } }
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "input[name=?]", "setting[openai_uri_base]" do |inputs|
|
||||
assert_equal "https://api.example.com/v1", inputs.first["value"]
|
||||
end
|
||||
end
|
||||
ensure
|
||||
Setting.openai_uri_base = nil
|
||||
Setting.openai_model = nil
|
||||
end
|
||||
|
||||
# PR #1862 review (jjmata): symmetric coverage for the model field. When the
|
||||
# user changes the URI base and clears the model in the same auto-submit, the
|
||||
# cross-field validation fails — the re-rendered model input must reflect the
|
||||
# user's submitted (cleared) value, not silently revert to the saved model.
|
||||
test "preserves submitted openai model in form when validation fails" do
|
||||
with_self_hosting do
|
||||
Setting.openai_uri_base = "https://saved.example.com/v1"
|
||||
Setting.openai_model = "saved-model"
|
||||
|
||||
patch settings_hosting_url, params: { setting: {
|
||||
openai_uri_base: "https://new.example.com/v1",
|
||||
openai_model: ""
|
||||
} }
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "input[name=?]", "setting[openai_uri_base]" do |inputs|
|
||||
assert_equal "https://new.example.com/v1", inputs.first["value"]
|
||||
end
|
||||
assert_select "input[name=?]", "setting[openai_model]" do |inputs|
|
||||
assert_not_equal "saved-model", inputs.first["value"].to_s,
|
||||
"model field must reflect the submitted (cleared) value, not the saved model"
|
||||
end
|
||||
end
|
||||
ensure
|
||||
Setting.openai_uri_base = nil
|
||||
Setting.openai_model = nil
|
||||
end
|
||||
|
||||
test "can update openai model alone when self hosting is enabled" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { openai_model: "gpt-4" } }
|
||||
|
||||
@@ -382,4 +382,41 @@ class AccountTest < ActiveSupport::TestCase
|
||||
assert_equal [ provider_holding.id, second_provider_holding.id ].sort, account.current_holdings.pluck(:id).sort
|
||||
assert_equal %w[CHF EUR], account.current_holdings.pluck(:currency).sort
|
||||
end
|
||||
|
||||
test "on account destroyed cascade transfer destroyed" do
|
||||
outflow_account = @family.accounts.create!({
|
||||
owner: @admin,
|
||||
name: "test_account_outflow",
|
||||
balance: 100,
|
||||
currency: "USD",
|
||||
accountable_type: "Depository",
|
||||
accountable_attributes: {}
|
||||
})
|
||||
inflow_account = @family.accounts.create!({
|
||||
owner: @admin,
|
||||
name: "test_account_inflow",
|
||||
balance: 100,
|
||||
currency: "USD",
|
||||
accountable_type: "Depository",
|
||||
accountable_attributes: {}
|
||||
})
|
||||
|
||||
transfer = create_transfer(
|
||||
from_account: outflow_account,
|
||||
to_account: inflow_account,
|
||||
amount: 50
|
||||
)
|
||||
|
||||
outflow_transaction = transfer.outflow_transaction
|
||||
|
||||
outflow_transaction.reload
|
||||
assert_equal "funds_movement", outflow_transaction.kind
|
||||
|
||||
inflow_account.destroy!
|
||||
|
||||
assert_raises(ActiveRecord::RecordNotFound) { transfer.reload }
|
||||
|
||||
outflow_transaction.reload
|
||||
assert_equal "standard", outflow_transaction.kind
|
||||
end
|
||||
end
|
||||
|
||||
@@ -213,6 +213,163 @@ class IbkrAccount::HistoricalBalancesSyncTest < ActiveSupport::TestCase
|
||||
assert_not_nil @account.balances.find_by(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
end
|
||||
|
||||
test "computes net_market_flows from equity delta minus trade flows" do
|
||||
# Day 1: total=3000, cash=500, non_cash=2500
|
||||
# Day 2: total=3200, cash=500, non_cash=2700 (Δnon_cash=200)
|
||||
# Buy trade on Day 2: CHF 150 (same currency as account, no FX)
|
||||
# nmf = 200 - 150 = 50
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
{ report_date: "2026-05-07", total: "3000.00" },
|
||||
{ report_date: "2026-05-08", total: "3200.00" }
|
||||
]
|
||||
)
|
||||
seed_balance(date: Date.new(2026, 5, 7), balance: 3000.00, cash_balance: 500.00)
|
||||
seed_balance(date: Date.new(2026, 5, 8), balance: 3200.00, cash_balance: 500.00)
|
||||
|
||||
security = Security.create!(ticker: "TEST", name: "Test Stock")
|
||||
@account.entries.create!(
|
||||
name: "Buy 100 TEST",
|
||||
date: Date.new(2026, 5, 8),
|
||||
amount: 150.00,
|
||||
currency: "CHF",
|
||||
entryable: Trade.new(security: security, qty: 100, price: 1.5, currency: "CHF")
|
||||
)
|
||||
|
||||
IbkrAccount::HistoricalBalancesSync.new(@ibkr_account).sync!
|
||||
|
||||
day1 = @account.balances.find_by!(date: Date.new(2026, 5, 7), currency: "CHF")
|
||||
day2 = @account.balances.find_by!(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
|
||||
assert_equal BigDecimal("0"), day1.net_market_flows
|
||||
assert_equal BigDecimal("50"), day2.net_market_flows
|
||||
|
||||
# Virtual column must still resolve to IBKR's equity total minus cash
|
||||
assert_equal BigDecimal("2500.00"), day1.end_non_cash_balance
|
||||
assert_equal BigDecimal("2700.00"), day2.end_non_cash_balance
|
||||
end
|
||||
|
||||
test "applies fx_rate_to_base when trade currency differs from account currency" do
|
||||
# Trade in EUR with fx_rate_to_base=1.1 → CHF 165, not CHF 150
|
||||
# Day 1: non_cash=2500, Day 2: non_cash=2700 (Δ=200)
|
||||
# nmf = 200 - 165 = 35
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
{ report_date: "2026-05-07", total: "3000.00" },
|
||||
{ report_date: "2026-05-08", total: "3200.00" }
|
||||
]
|
||||
)
|
||||
seed_balance(date: Date.new(2026, 5, 7), balance: 3000.00, cash_balance: 500.00)
|
||||
seed_balance(date: Date.new(2026, 5, 8), balance: 3200.00, cash_balance: 500.00)
|
||||
|
||||
security = Security.create!(ticker: "TEST2", name: "Test Stock EUR")
|
||||
trade = Trade.new(security: security, qty: 100, price: 1.5, currency: "EUR")
|
||||
trade.exchange_rate = 1.1
|
||||
@account.entries.create!(
|
||||
name: "Buy 100 TEST2",
|
||||
date: Date.new(2026, 5, 8),
|
||||
amount: 150.00,
|
||||
currency: "EUR",
|
||||
entryable: trade
|
||||
)
|
||||
|
||||
IbkrAccount::HistoricalBalancesSync.new(@ibkr_account).sync!
|
||||
|
||||
day2 = @account.balances.find_by!(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
assert_in_delta 35.0, day2.net_market_flows.to_f, 0.01
|
||||
assert_equal BigDecimal("2700.00"), day2.end_non_cash_balance
|
||||
end
|
||||
|
||||
test "excludes balance row from upsert when Money::ConversionError prevents FX conversion" do
|
||||
# EUR trade with no exchange_rate stored → custom_rate=nil → ConversionError raised.
|
||||
# The affected date is excluded from the upsert entirely so net_market_flows is not
|
||||
# silently wrong (the trade's value would otherwise flow into market appreciation).
|
||||
# The seeded day2 balance is intentionally different from IBKR's total (3150 vs 3200)
|
||||
# so we can assert the row was not overwritten by sync.
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
{ report_date: "2026-05-07", total: "3000.00" },
|
||||
{ report_date: "2026-05-08", total: "3200.00" }
|
||||
]
|
||||
)
|
||||
seed_balance(date: Date.new(2026, 5, 7), balance: 3000.00, cash_balance: 500.00)
|
||||
seed_balance(date: Date.new(2026, 5, 8), balance: 3150.00, cash_balance: 500.00)
|
||||
|
||||
security = Security.create!(ticker: "NORATE", name: "No Rate EUR Stock")
|
||||
@account.entries.create!(
|
||||
name: "Buy 100 NORATE",
|
||||
date: Date.new(2026, 5, 8),
|
||||
amount: 150.00,
|
||||
currency: "EUR",
|
||||
entryable: Trade.new(security: security, qty: 100, price: 1.5, currency: "EUR")
|
||||
)
|
||||
|
||||
Money.any_instance.stubs(:exchange_to).raises(
|
||||
Money::ConversionError.new(from_currency: "EUR", to_currency: "CHF", date: Date.new(2026, 5, 8))
|
||||
)
|
||||
|
||||
IbkrAccount::HistoricalBalancesSync.new(@ibkr_account).sync!
|
||||
|
||||
# Day 1 is unaffected — still synced normally
|
||||
day1 = @account.balances.find_by!(date: Date.new(2026, 5, 7), currency: "CHF")
|
||||
assert_equal BigDecimal("3000.00"), day1.balance
|
||||
|
||||
# Day 2 was excluded from the upsert — seeded values are preserved, not overwritten
|
||||
day2 = @account.balances.find_by!(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
assert_equal BigDecimal("3150.00"), day2.balance # seeded, not IBKR's 3200
|
||||
assert_equal BigDecimal("0"), day2.net_market_flows # seeded, not recomputed
|
||||
end
|
||||
|
||||
test "net_market_flows equals full non_cash delta when account has no trades" do
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
{ report_date: "2026-05-07", total: "3000.00" },
|
||||
{ report_date: "2026-05-08", total: "3300.00" }
|
||||
]
|
||||
)
|
||||
seed_balance(date: Date.new(2026, 5, 7), balance: 3000.00, cash_balance: 500.00)
|
||||
seed_balance(date: Date.new(2026, 5, 8), balance: 3300.00, cash_balance: 500.00)
|
||||
|
||||
IbkrAccount::HistoricalBalancesSync.new(@ibkr_account).sync!
|
||||
|
||||
day1 = @account.balances.find_by!(date: Date.new(2026, 5, 7), currency: "CHF")
|
||||
day2 = @account.balances.find_by!(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
|
||||
assert_equal BigDecimal("0"), day1.net_market_flows
|
||||
assert_equal BigDecimal("300"), day2.net_market_flows
|
||||
assert_equal BigDecimal("2800.00"), day2.end_non_cash_balance
|
||||
end
|
||||
|
||||
test "sell trades reduce net_buy_sell so market loss is isolated in net_market_flows" do
|
||||
# Day 1: total=3000, cash=500, non_cash=2500
|
||||
# Day 2: total=2700, cash=700, non_cash=2000 (Δnon_cash=-500)
|
||||
# Sell 100 at CHF 1.50: entry.amount=-150 (negative = proceeds received)
|
||||
# net_buy_sell=-150; nmf = -500 - (-150) = -350 (market caused -350 loss)
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
{ report_date: "2026-05-07", total: "3000.00" },
|
||||
{ report_date: "2026-05-08", total: "2700.00" }
|
||||
]
|
||||
)
|
||||
seed_balance(date: Date.new(2026, 5, 7), balance: 3000.00, cash_balance: 500.00)
|
||||
seed_balance(date: Date.new(2026, 5, 8), balance: 2700.00, cash_balance: 700.00)
|
||||
|
||||
security = Security.create!(ticker: "SELL_TEST", name: "Sell Test Stock")
|
||||
@account.entries.create!(
|
||||
name: "Sell 100 SELL_TEST",
|
||||
date: Date.new(2026, 5, 8),
|
||||
amount: -150.00,
|
||||
currency: "CHF",
|
||||
entryable: Trade.new(security: security, qty: -100, price: 1.5, currency: "CHF")
|
||||
)
|
||||
|
||||
IbkrAccount::HistoricalBalancesSync.new(@ibkr_account).sync!
|
||||
|
||||
day2 = @account.balances.find_by!(date: Date.new(2026, 5, 8), currency: "CHF")
|
||||
assert_equal BigDecimal("-350"), day2.net_market_flows
|
||||
assert_equal BigDecimal("2000.00"), day2.end_non_cash_balance
|
||||
end
|
||||
|
||||
test "writes balance row with zero total for fully liquidated dates" do
|
||||
@ibkr_account.update!(
|
||||
raw_equity_summary_payload: [
|
||||
|
||||
Reference in New Issue
Block a user