insika 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +21 -9
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
|
@@ -7,14 +7,14 @@ require_relative "config"
|
|
|
7
7
|
|
|
8
8
|
module Insika
|
|
9
9
|
module Safety
|
|
10
|
-
# Input guardrail as a Middleware
|
|
10
|
+
# Input guardrail as a Middleware. It sits on the ONE seam that
|
|
11
11
|
# already short-circuits structurally (stage 4: a link that does not call `nxt`),
|
|
12
12
|
# and uses the NEW graceful-halt contract: instead of `halt_reason` (which the
|
|
13
13
|
# Executor maps to a turn FAILURE), it sets `halt_response` (a safe reply) +
|
|
14
14
|
# `guardrail_block` (audit metadata) and returns without calling `nxt`. The
|
|
15
15
|
# Executor completes the turn with that safe reply, never touching the LLM.
|
|
16
16
|
#
|
|
17
|
-
# Two tiers
|
|
17
|
+
# Two tiers:
|
|
18
18
|
# 1. deterministic scan (always, cheap, zero-token) — Detectors#scan_input;
|
|
19
19
|
# 2. LLM moderator (opt-in per agent) — only when the deterministic tier let
|
|
20
20
|
# the message through, so the cheap layer short-circuits the expensive one.
|
|
@@ -23,7 +23,7 @@ module Insika
|
|
|
23
23
|
# link lives ONCE in the global MiddlewareStack — there is no per-agent stack.
|
|
24
24
|
#
|
|
25
25
|
# `moderator_factory` (optional): ->(config) { Moderator | nil }, built by the
|
|
26
|
-
# Safety::Factory. nil = deterministic only (
|
|
26
|
+
# Safety::Factory. nil = deterministic only (B parity).
|
|
27
27
|
class InputGuardrail < Insika::Middleware
|
|
28
28
|
def initialize(moderator_factory: nil)
|
|
29
29
|
@moderator_factory = moderator_factory
|
|
@@ -33,7 +33,7 @@ module Insika
|
|
|
33
33
|
config = Config.from_profile(state.profile)
|
|
34
34
|
return nxt.call(state) unless config.input
|
|
35
35
|
|
|
36
|
-
hit = Detectors.scan_input(state.message.to_s, categories: config.input_categories)
|
|
36
|
+
hit = Detectors.scan_input(state.message.to_s, categories: config.input_categories, corpus: config.corpus)
|
|
37
37
|
return block(state, config, category: hit[:category], source: :deterministic, detail: hit[:matched]) if hit
|
|
38
38
|
|
|
39
39
|
if config.moderator? && (mod = build_moderator(config))
|
|
@@ -43,6 +43,11 @@ module Insika
|
|
|
43
43
|
return block(state, config, category: category, source: :moderator,
|
|
44
44
|
detail: verdict.reason, action: verdict.action)
|
|
45
45
|
end
|
|
46
|
+
# an UNAVAILABLE moderator fails open — the turn proceeds —
|
|
47
|
+
# but silence is not a negative: record the degradation on the state so the
|
|
48
|
+
# Executor (single emitter) emits :guardrail_flagged and a degraded tier
|
|
49
|
+
# never looks identical to a healthy one in the audit stream.
|
|
50
|
+
flag_unavailable(state, verdict) if verdict.unavailable?
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
nxt.call(state)
|
|
@@ -54,11 +59,21 @@ module Insika
|
|
|
54
59
|
@moderator_factory&.call(config)
|
|
55
60
|
end
|
|
56
61
|
|
|
62
|
+
# Appends the audit flag for a degraded moderator tier. Same
|
|
63
|
+
# state-carried channel the OutputValidator uses; detail is the moderator's
|
|
64
|
+
# own reason string ("moderator error (fail-open)" / "unparseable
|
|
65
|
+
# (fail-open)"), never message content.
|
|
66
|
+
def flag_unavailable(state, verdict)
|
|
67
|
+
state.guardrail_flags = Array(state.guardrail_flags) + [{
|
|
68
|
+
category: "moderator_unavailable", source: "moderator", detail: verdict.reason.to_s[0, 200]
|
|
69
|
+
}]
|
|
70
|
+
end
|
|
71
|
+
|
|
57
72
|
# The block category. `escalate` (an ACTION) becomes the `escalate` category so
|
|
58
73
|
# the agent can address it; otherwise the moderator's own category flows
|
|
59
74
|
# through UNCOLLAPSED — the safe-reply lookup (SafeResponses.for) resolves an
|
|
60
75
|
# unknown one to the agent's `default` / the neutral built-in, so we don't need
|
|
61
|
-
# to force it into a fixed bucket here (configuration over convention
|
|
76
|
+
# to force it into a fixed bucket here (configuration over convention).
|
|
62
77
|
def moderator_category(verdict)
|
|
63
78
|
return :escalate if verdict.action.to_s == "escalate"
|
|
64
79
|
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Safety
|
|
7
|
-
# LLM content moderator
|
|
7
|
+
# LLM content moderator. The subtle tier the regex can't
|
|
8
8
|
# reach: social engineering (a fabricated prior promise), veiled hostility, tone.
|
|
9
9
|
#
|
|
10
10
|
# Pure over an injected `ask` callable (prompt -> raw model text), exactly like
|
|
@@ -13,48 +13,56 @@ module Insika
|
|
|
13
13
|
#
|
|
14
14
|
# FAIL-OPEN by construction: the deterministic layer already ran and caught the
|
|
15
15
|
# gross cases, so an unparseable/failed moderator reply must NOT block a
|
|
16
|
-
# legitimate customer
|
|
17
|
-
#
|
|
16
|
+
# legitimate customer. Blocking is the high-stakes direction (a false
|
|
17
|
+
# positive turns away a real buyer). But fail-open is not a fake negative
|
|
18
|
+
# a moderator that could not answer returns the third state
|
|
19
|
+
# `unavailable` — it does not block, and it is NOT recorded as a clean `allow`,
|
|
20
|
+
# so a degraded tier is distinguishable from a healthy one in the audit stream.
|
|
18
21
|
class Moderator
|
|
19
22
|
Verdict = Struct.new(:category, :action, :reason, keyword_init: true) do
|
|
20
23
|
def block? = %w[refuse escalate].include?(action.to_s)
|
|
24
|
+
def unavailable? = action.to_s == "unavailable"
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
CATEGORIES = %w[injection abuse sexual self_harm off_topic safe].freeze
|
|
24
|
-
ACTIONS = %w[allow refuse escalate].freeze
|
|
28
|
+
ACTIONS = %w[allow refuse escalate unavailable].freeze
|
|
25
29
|
|
|
26
30
|
# ask: ->(prompt) { "<raw model text>" }.
|
|
27
31
|
def initialize(ask:)
|
|
28
32
|
@ask = ask
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
# Classifies a user message. Returns a Verdict; on ANY failure ->
|
|
32
|
-
# (fail-open
|
|
35
|
+
# Classifies a user message. Returns a Verdict; on ANY failure -> unavailable
|
|
36
|
+
# (fail-open: never blocks, but never masquerades as a real `allow` either —
|
|
37
|
+
# `context` is optional free text (e.g. the agent's domain) woven
|
|
33
38
|
# into the prompt.
|
|
34
39
|
def classify(message, context: nil)
|
|
35
40
|
raw = @ask.call(build_prompt(message.to_s, context)).to_s
|
|
36
41
|
parse(raw)
|
|
37
42
|
rescue StandardError
|
|
38
|
-
Verdict.new(category: "safe", action: "
|
|
43
|
+
Verdict.new(category: "safe", action: "unavailable", reason: "moderator error (fail-open)")
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
private
|
|
42
47
|
|
|
43
48
|
def parse(raw)
|
|
44
49
|
json = raw[/\{.*\}/m]
|
|
45
|
-
return
|
|
50
|
+
return unavailable_verdict unless json
|
|
46
51
|
|
|
47
52
|
data = JSON.parse(json)
|
|
48
53
|
action = data["action"].to_s.strip.downcase
|
|
49
|
-
action
|
|
54
|
+
# An out-of-enum action is a reply we cannot honor — normalize to
|
|
55
|
+
# unavailable, not allow: inventing a negative the model never gave is the
|
|
56
|
+
# same ambiguity removes.
|
|
57
|
+
action = "unavailable" unless ACTIONS.include?(action)
|
|
50
58
|
category = data["category"].to_s.strip.downcase
|
|
51
59
|
category = "safe" unless CATEGORIES.include?(category)
|
|
52
60
|
Verdict.new(category: category, action: action, reason: data["reason"].to_s)
|
|
53
61
|
rescue JSON::ParserError
|
|
54
|
-
|
|
62
|
+
unavailable_verdict
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
def
|
|
65
|
+
def unavailable_verdict = Verdict.new(category: "safe", action: "unavailable", reason: "unparseable (fail-open)")
|
|
58
66
|
|
|
59
67
|
def build_prompt(message, context)
|
|
60
68
|
<<~PROMPT
|
|
@@ -4,12 +4,12 @@ require_relative "detectors"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Safety
|
|
7
|
-
# Deterministic output redaction on the STREAM
|
|
7
|
+
# Deterministic output redaction on the STREAM. The turn
|
|
8
8
|
# streams `content` deltas, so we cannot "unsay" text already sent — the filter
|
|
9
9
|
# sits between the provider chunks and the `:content` event and only ever emits
|
|
10
10
|
# text it has PROVEN clean.
|
|
11
11
|
#
|
|
12
|
-
# The hard part (
|
|
12
|
+
# The hard part ("fronteira de chunk"): a CPF/secret can arrive split
|
|
13
13
|
# across two chunks and neither half matches on its own. A naive delta-by-delta
|
|
14
14
|
# regex leaks "by construction". So the filter keeps a SLIDING BUFFER: it retains
|
|
15
15
|
# the tail that might still be growing into a match (Detectors::OPEN_TAIL, which
|
|
@@ -21,7 +21,11 @@ module Insika
|
|
|
21
21
|
class OutputFilter
|
|
22
22
|
attr_reader :redaction_counts
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
# `corpus` : the compiled corpus for the turn — the
|
|
25
|
+
# language-filtered PII set and its open-tail pattern. Default = the
|
|
26
|
+
# full shipped corpus (parity).
|
|
27
|
+
def initialize(corpus: Corpus.compile)
|
|
28
|
+
@corpus = corpus
|
|
25
29
|
@buf = +""
|
|
26
30
|
@emitted = +"" # full redacted text emitted so far (for the persisted content)
|
|
27
31
|
@redaction_counts = Hash.new(0)
|
|
@@ -51,11 +55,11 @@ module Insika
|
|
|
51
55
|
def safe_cut
|
|
52
56
|
cut = @buf.length
|
|
53
57
|
|
|
54
|
-
if (m = @buf.match(
|
|
58
|
+
if (m = @buf.match(@corpus.open_tail))
|
|
55
59
|
cut = [cut, m.begin(0)].min
|
|
56
60
|
end
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
@corpus.match_ranges(@buf).each do |b, e|
|
|
59
63
|
cut = b if b < cut && cut < e
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -69,7 +73,7 @@ module Insika
|
|
|
69
73
|
|
|
70
74
|
chunk = @buf[0...cut]
|
|
71
75
|
@buf = @buf[cut..] || +""
|
|
72
|
-
redacted, counts =
|
|
76
|
+
redacted, counts = @corpus.redact(chunk)
|
|
73
77
|
counts.each { |name, n| @redaction_counts[name] += n }
|
|
74
78
|
@emitted << redacted
|
|
75
79
|
redacted
|
|
@@ -6,11 +6,11 @@ require_relative "config"
|
|
|
6
6
|
|
|
7
7
|
module Insika
|
|
8
8
|
module Safety
|
|
9
|
-
# Post-turn output validator
|
|
9
|
+
# Post-turn output validator. Runs as an `after_task` hook:
|
|
10
10
|
# with the FINAL assistant text assembled, it does a richer check than the
|
|
11
11
|
# stream filter can — an unverified discount/price promise, a tone slip — and
|
|
12
12
|
# FLAGS it (audit) rather than blocking. It is honest about the streaming limit
|
|
13
|
-
#
|
|
13
|
+
# for a streaming agent the text is already out the door, so this is
|
|
14
14
|
# detection, not prevention; true pre-emission blocking needs `streaming:false`
|
|
15
15
|
# (a per-profile override left for a later slice).
|
|
16
16
|
#
|
|
@@ -27,13 +27,19 @@ module Insika
|
|
|
27
27
|
class OutputValidator
|
|
28
28
|
# `ask_factory` (optional): ->(config) { ->(prompt){text} | nil }, built by the
|
|
29
29
|
# Safety::Factory from the utility_model. nil = deterministic only.
|
|
30
|
-
|
|
30
|
+
# `grounding` (optional): a GroundingValidator — its step runs
|
|
31
|
+
# FIRST in #call, before the `config.output` gate (D9: grounding is
|
|
32
|
+
# evidence integrity, independent of the guardrails opt-in).
|
|
33
|
+
def initialize(ask_factory: nil, grounding: nil)
|
|
31
34
|
@ask_factory = ask_factory
|
|
35
|
+
@grounding = grounding
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
# after_task hook body. Idempotent and defensive: never raises out (a hook
|
|
35
|
-
# error must not fail a committed turn).
|
|
39
|
+
# error must not fail a committed turn). Grounding runs BEFORE the output
|
|
40
|
+
# gate so an agent with guardrails off and grounding on still gets the check.
|
|
36
41
|
def call(state)
|
|
42
|
+
state = @grounding&.call(state) || state
|
|
37
43
|
config = Config.from_profile(state.profile)
|
|
38
44
|
return state unless config.output
|
|
39
45
|
|
|
@@ -41,7 +47,7 @@ module Insika
|
|
|
41
47
|
return state if text.empty?
|
|
42
48
|
|
|
43
49
|
flags = []
|
|
44
|
-
flags.concat(deterministic_flags(text))
|
|
50
|
+
flags.concat(deterministic_flags(text, config.corpus))
|
|
45
51
|
flags.concat(llm_flags(text, config)) if config.moderator?
|
|
46
52
|
|
|
47
53
|
state.guardrail_flags = Array(state.guardrail_flags) + flags unless flags.empty?
|
|
@@ -55,8 +61,8 @@ module Insika
|
|
|
55
61
|
# Residual PII/secret that somehow reached the final text (the stream filter
|
|
56
62
|
# should have caught it — this is defense in depth, and the flag itself
|
|
57
63
|
# carries category counts, never the raw value).
|
|
58
|
-
def deterministic_flags(text)
|
|
59
|
-
_redacted, counts = Detectors.redact(text)
|
|
64
|
+
def deterministic_flags(text, corpus)
|
|
65
|
+
_redacted, counts = Detectors.redact(text, corpus: corpus)
|
|
60
66
|
return [] if counts.empty?
|
|
61
67
|
|
|
62
68
|
[{ category: "pii_residual", source: "deterministic", detail: counts.map { |k, v| "#{k}:#{v}" }.join(",") }]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
4
|
module Safety
|
|
5
|
-
# Safe reply for a blocked turn
|
|
5
|
+
# Safe reply for a blocked turn. A blocked turn NEVER returns a
|
|
6
6
|
# raw error nor silence — it completes gracefully with one of these.
|
|
7
7
|
#
|
|
8
8
|
# CONFIGURATION OVER CONVENTION: this is OSS across arbitrary businesses and
|
|
@@ -10,8 +10,8 @@ module Insika
|
|
|
10
10
|
# sandbox primitive — always on, independent of the exec provider (local or
|
|
11
11
|
# docker) and of the engine's approval layer.
|
|
12
12
|
#
|
|
13
|
-
# Extracted VERBATIM from the insika-code prototype's `Workspace`
|
|
14
|
-
# and promoted to a core primitive
|
|
13
|
+
# Extracted VERBATIM from the insika-code prototype's `Workspace`
|
|
14
|
+
# and promoted to a core primitive. The two layers of defense:
|
|
15
15
|
# 1. `File.expand_path` normalizes `..` traversal; the expanded path must be
|
|
16
16
|
# the root itself or a descendant of it (string containment on a
|
|
17
17
|
# separator boundary, so `/ws-evil` does not pass for root `/ws`).
|
data/lib/insika/sandbox.rb
CHANGED
|
@@ -7,7 +7,7 @@ require_relative "sandbox/local"
|
|
|
7
7
|
require_relative "sandbox/docker"
|
|
8
8
|
|
|
9
9
|
module Insika
|
|
10
|
-
# Sandbox primitive (
|
|
10
|
+
# Sandbox primitive (COMPETITIVE-ANALYSIS): a single, pluggable
|
|
11
11
|
# interface for confined execution, promoted to the core from the insika-code
|
|
12
12
|
# prototype. Two halves:
|
|
13
13
|
#
|
data/lib/insika/schema_guard.rb
CHANGED
|
@@ -131,6 +131,41 @@ module Insika
|
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Walks a dotted path on a plain object: nil when a segment
|
|
135
|
+
# is absent or the intermediate is not a Hash. Shared by the evidence output
|
|
136
|
+
# check and the Processor's extraction.
|
|
137
|
+
def dig(obj, path)
|
|
138
|
+
path.to_s.split(".").reduce(obj) do |cur, seg|
|
|
139
|
+
return nil unless cur.is_a?(Hash) && cur.key?(seg)
|
|
140
|
+
|
|
141
|
+
cur[seg]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The evidence RESULT contract: {items: [{id, line}]} .
|
|
146
|
+
# -> nil | String. Same idiom as `violation`: nil = fine, one message = what
|
|
147
|
+
# is wrong. A malformed evidence result is a correctable TOOL answer — the
|
|
148
|
+
# envelope returns it to the model as `{error:}`, exactly like a malformed
|
|
149
|
+
# call is today. A raw body that is NOT an object (a bare JSON array from a
|
|
150
|
+
# search, a string, nil) is a violation — never a silent `{items: []}` that
|
|
151
|
+
# the model reads as "no products".
|
|
152
|
+
def violation_output(spec, raw)
|
|
153
|
+
return nil if spec.nil?
|
|
154
|
+
return "evidence: result must be an object" unless raw.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
items = dig(raw, spec.items_path)
|
|
157
|
+
return "evidence: items is missing" if items.nil?
|
|
158
|
+
return "evidence: items must be a list" unless items.is_a?(Array)
|
|
159
|
+
|
|
160
|
+
items.each_with_index do |item, i|
|
|
161
|
+
ok = item.is_a?(Hash) &&
|
|
162
|
+
Coercion.present?(item["id"] || item[:id]) &&
|
|
163
|
+
(item["line"] || item[:line]).is_a?(String)
|
|
164
|
+
return "evidence: items[#{i}] must be {id, line}" unless ok
|
|
165
|
+
end
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
134
169
|
# Name the shape the way a model reads it, not the way Ruby does.
|
|
135
170
|
def kind(value)
|
|
136
171
|
case value
|