insika 0.0.1 → 0.1.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 +295 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +351 -0
- data/docs/AGENTS.md +494 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +100 -0
- data/docs/DEPLOY.md +334 -0
- data/docs/EMBEDDING.md +194 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +231 -0
- data/docs/OBSERVABILITY.md +365 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +362 -0
- data/docs/SKILLS.md +98 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +188 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +150 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/chat_builder.rb +254 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/coercion.rb +50 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +41 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +71 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +50 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +85 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +19 -0
- data/lib/insika/context/priority.rb +29 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +108 -0
- data/lib/insika/context/providers/skill.rb +20 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +294 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +379 -0
- data/lib/insika/dsl/server_boot.rb +97 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +307 -0
- data/lib/insika/edge_limiter.rb +130 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +246 -0
- data/lib/insika/errors.rb +145 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +114 -0
- data/lib/insika/executor.rb +1680 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +114 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +103 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +121 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +137 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +127 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +87 -0
- data/lib/insika/safety/moderator.rb +86 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/admin_auth.rb +29 -0
- data/lib/insika/server/app.rb +850 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +110 -0
- data/lib/insika/server/responses.rb +155 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +113 -0
- data/lib/insika/skill_store.rb +79 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1571 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +69 -0
- data/lib/insika/studio/forms.rb +340 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +62 -0
- data/lib/insika/studio/views/settings.erb +173 -0
- data/lib/insika/studio/views/skills.erb +86 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +220 -0
- data/lib/insika/tools/load_skill.rb +41 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +158 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +198 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +203 -4
- metadata +395 -8
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# DYNAMIC tool registry: composes the CODE registry (base, built at boot,
|
|
5
|
+
# immutable) with the DATA-DEFINED tools from the ToolStore. Drop-in for ToolRegistry —
|
|
6
|
+
# the Executor/ToolCatalog/ToolEnvelope only use entries/resolve/side_effect?. Phase 5,
|
|
7
|
+
# Step B / D2.
|
|
8
|
+
#
|
|
9
|
+
# Rules:
|
|
10
|
+
# - COLLISION: the base (code) ALWAYS wins — a data-tool cannot hijack
|
|
11
|
+
# the name of a code tool (security, R3). The authoring Command also
|
|
12
|
+
# refuses to create with a colliding name (code_tool?), but the defense stays here.
|
|
13
|
+
# - HOT: `reload` re-reads the store and swaps the dynamic index atomically — a
|
|
14
|
+
# new/edited data-tool takes effect on the next turn without a restart (F5), mirroring
|
|
15
|
+
# SkillCatalog.reload. An in-flight turn has already captured the index.
|
|
16
|
+
# - PARITY (NF1): empty ToolStore ⇒ entries/resolve/side_effect? identical to the
|
|
17
|
+
# pure base. The base (config/wiring.rb) does not even use the overlay — zero regression.
|
|
18
|
+
#
|
|
19
|
+
# The data-tools enter as NORMAL Registry::Entry (optional: false) — they obey
|
|
20
|
+
# the same per-agent allow/deny as code tools; exposure is the operator's
|
|
21
|
+
# (the /tools matrix), not automatic just because they are "data-defined".
|
|
22
|
+
class OverlayToolRegistry
|
|
23
|
+
def initialize(base:, tool_store:, http:, egress: Insika::EgressGuard, egress_options: {}, event_stream: nil)
|
|
24
|
+
@base = base
|
|
25
|
+
@tool_store = tool_store
|
|
26
|
+
@http = http
|
|
27
|
+
@egress = egress
|
|
28
|
+
@egress_options = egress_options
|
|
29
|
+
@event_stream = event_stream
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Base + dynamic, except dynamic ones that collide with the base (base wins).
|
|
33
|
+
def entries
|
|
34
|
+
@base.entries + dynamic.reject { |e| code_tool?(e.name) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def names
|
|
38
|
+
(@base.names + dynamic.map(&:name)).uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# -> instance (base wins) | raise NotFoundError.
|
|
42
|
+
def resolve(name)
|
|
43
|
+
key = name.to_s
|
|
44
|
+
return @base.resolve(key) if code_tool?(key)
|
|
45
|
+
|
|
46
|
+
entry = dynamic.find { |e| e.name == key }
|
|
47
|
+
raise Insika::NotFoundError, "'#{name}' not registered in #{self.class}" unless entry
|
|
48
|
+
|
|
49
|
+
entry.factory.call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# -> bool; consumed by ToolEnvelope (checkpoint/skip-on-resume). Base wins.
|
|
53
|
+
def side_effect?(name)
|
|
54
|
+
key = name.to_s
|
|
55
|
+
return @base.side_effect?(key) if code_tool?(key)
|
|
56
|
+
|
|
57
|
+
entry = dynamic.find { |e| e.name == key }
|
|
58
|
+
entry ? !!entry.metadata[:side_effect] : false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Atomic swap of the dynamic index (after writing/removing a data-tool).
|
|
62
|
+
def reload
|
|
63
|
+
@dynamic = build_dynamic
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Is it a CODE tool (base)? Used by the authoring Command's validation.
|
|
68
|
+
def code_tool?(name) = @base.names.include?(name.to_s)
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def dynamic
|
|
73
|
+
@dynamic ||= build_dynamic
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_dynamic
|
|
77
|
+
@tool_store.all_raw.filter_map { |raw| entry_for(raw) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def entry_for(raw)
|
|
81
|
+
definition = Insika::ToolDefinition.from_h(raw)
|
|
82
|
+
Insika::Registry::Entry.new(
|
|
83
|
+
name: definition.name, plugin: "data-tools",
|
|
84
|
+
metadata: { optional: false, side_effect: definition.side_effect,
|
|
85
|
+
group: definition.group, tags: definition.tags },
|
|
86
|
+
factory: -> { build_tool(definition) }
|
|
87
|
+
)
|
|
88
|
+
rescue Insika::ValidationError => e
|
|
89
|
+
warn "[overlay-tools] invalid definition ignored: #{e.message}"
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Lazy require: DataDefinedTool inherits from RubyLLM::Tool (pulls in the gem) -> kept
|
|
94
|
+
# out of insika.rb load-time, loaded on the 1st instance (turn time).
|
|
95
|
+
def build_tool(definition)
|
|
96
|
+
require_relative "tools/data_defined_tool"
|
|
97
|
+
Insika::Tools::DataDefinedTool.new(
|
|
98
|
+
definition: definition, http: @http, egress: @egress,
|
|
99
|
+
egress_options: @egress_options, event_stream: @event_stream
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/insika/pack.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
# Provisioning pack (Phase 6/D4/F6): the PORTABLE form of an agent — a
|
|
7
|
+
# manifest + the prompt files + the skills + the data-tool defs. It's what
|
|
8
|
+
# `docs/prompt-base/06` describes as a workspace, here in a value object
|
|
9
|
+
# consumable by the PackImporter (which emits the authoring Commands). GENERIC
|
|
10
|
+
# per project (NF1): the engine doesn't know achei-b2b — the pack is the contract.
|
|
11
|
+
#
|
|
12
|
+
# config: Hash — manifest (AgentProfile.build attrs: id/model/provider/
|
|
13
|
+
# limits/metadata/tools_deferred/…). `id`/`model` required there.
|
|
14
|
+
# files: { "IDENTITY.md" => "<content>", ... } — prompt files (become
|
|
15
|
+
# prompt_files via write_agent_file). File name = key.
|
|
16
|
+
# skills: { "escalation-to-human" => "<SKILL.md>", ... } — 1 per skill.
|
|
17
|
+
# tools: [ { ToolDefinition hash }, ... ] — the pack's data-tool defs.
|
|
18
|
+
#
|
|
19
|
+
# Two sources: `from_h` (GatewayClient JSON — provisioning API, task 8) and
|
|
20
|
+
# `from_dir` (a folder on disk per docs/prompt-base/06 — authoring/CLI).
|
|
21
|
+
Pack = Data.define(:config, :files, :skills, :tools) do
|
|
22
|
+
# Raw Hash (string|symbol keys) -> Pack. Tolerates both key conventions
|
|
23
|
+
# (the JSON wire may arrive symbolized or not).
|
|
24
|
+
def self.from_h(hash)
|
|
25
|
+
h = hash || {}
|
|
26
|
+
new(
|
|
27
|
+
config: symbolize(dig(h, :config) || {}),
|
|
28
|
+
files: text_values!(stringify_keys(dig(h, :files) || {}), "files"),
|
|
29
|
+
skills: text_values!(stringify_keys(dig(h, :skills) || {}), "skills"),
|
|
30
|
+
tools: Array(dig(h, :tools))
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A pack's files and skills are NAME => MARKDOWN. Only the keys used to be
|
|
35
|
+
# normalized, so `{"files": {"AGENTS.md": {"content": "…"}}}` — the shape anyone
|
|
36
|
+
# writes first — was accepted and reached the store as Ruby's #inspect of the
|
|
37
|
+
# object. Refuse at the front door, where the name of the offending file is still
|
|
38
|
+
# in hand.
|
|
39
|
+
def self.text_values!(hash, field)
|
|
40
|
+
hash.each do |name, value|
|
|
41
|
+
next if value.is_a?(String)
|
|
42
|
+
|
|
43
|
+
raise Insika::ValidationError,
|
|
44
|
+
"pack #{field}['#{name}'] must be the file's text, got #{value.class} — " \
|
|
45
|
+
"a nested object (e.g. {\"content\": …}) is not unwrapped, it is stored verbatim"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
private_class_method :text_values!
|
|
49
|
+
|
|
50
|
+
# Folder on disk (docs/prompt-base/06): `agent.config.json` + `*.md` at the
|
|
51
|
+
# root + `skills/<name>/SKILL.md` + `tools/*.json` (1 ToolDefinition per file).
|
|
52
|
+
def self.from_dir(path)
|
|
53
|
+
root = path.to_s
|
|
54
|
+
config_file = File.join(root, "agent.config.json")
|
|
55
|
+
raise Insika::ValidationError, "pack missing agent.config.json in #{root}" unless File.exist?(config_file)
|
|
56
|
+
|
|
57
|
+
new(
|
|
58
|
+
config: symbolize(JSON.parse(File.read(config_file, encoding: "UTF-8"))),
|
|
59
|
+
files: read_md_files(root),
|
|
60
|
+
skills: read_skills(File.join(root, "skills")),
|
|
61
|
+
tools: read_tools(File.join(root, "tools"))
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.dig(hash, key) = hash[key] || hash[key.to_s]
|
|
66
|
+
private_class_method :dig
|
|
67
|
+
|
|
68
|
+
def self.symbolize(hash)
|
|
69
|
+
(hash || {}).each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
|
|
70
|
+
end
|
|
71
|
+
private_class_method :symbolize
|
|
72
|
+
|
|
73
|
+
def self.stringify_keys(hash)
|
|
74
|
+
(hash || {}).each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
|
|
75
|
+
end
|
|
76
|
+
private_class_method :stringify_keys
|
|
77
|
+
|
|
78
|
+
# `*.md` at the pack ROOT (doesn't descend into skills/). File name = key.
|
|
79
|
+
def self.read_md_files(root)
|
|
80
|
+
Dir.glob(File.join(root, "*.md")).sort.each_with_object({}) do |file, acc|
|
|
81
|
+
acc[File.basename(file)] = File.read(file, encoding: "UTF-8")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
private_class_method :read_md_files
|
|
85
|
+
|
|
86
|
+
# `skills/<name>/SKILL.md` -> { "<name>" => "<content>" }.
|
|
87
|
+
def self.read_skills(dir)
|
|
88
|
+
Dir.glob(File.join(dir, "*", "SKILL.md")).sort.each_with_object({}) do |file, acc|
|
|
89
|
+
acc[File.basename(File.dirname(file))] = File.read(file, encoding: "UTF-8")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
private_class_method :read_skills
|
|
93
|
+
|
|
94
|
+
# `tools/*.json` -> [ ToolDefinition hash, ... ].
|
|
95
|
+
def self.read_tools(dir)
|
|
96
|
+
Dir.glob(File.join(dir, "*.json")).sort.map do |file|
|
|
97
|
+
JSON.parse(File.read(file, encoding: "UTF-8"))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :read_tools
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Pack importer (Phase 6/D4/F6, task 7): reads a Pack and emits the ALREADY
|
|
5
|
+
# existing authoring Commands — create_agent/update_agent + write_agent_file +
|
|
6
|
+
# write_skill + write_data_tool — making an agent provisionable at runtime from
|
|
7
|
+
# a standardized pack. It's the piece that achei-b2b's `GatewayClient`/
|
|
8
|
+
# `ProvisionStore` triggers (via the provisioning API, task 8).
|
|
9
|
+
#
|
|
10
|
+
# GENERIC (NF1): nothing here mentions achei-b2b — the pack is the contract. Tool
|
|
11
|
+
# names come from the PACK, so prompts<->tools stay consistent by construction
|
|
12
|
+
# (NF2). It doesn't write to the store directly: it only dispatches Commands on
|
|
13
|
+
# the bus (the same transport discipline) + READS the ProfileSource to decide
|
|
14
|
+
# create vs update.
|
|
15
|
+
#
|
|
16
|
+
# IDEMPOTENT (upsert): re-importing the same pack reconciles — files/skills/
|
|
17
|
+
# tools rewritten; the allowlists (prompt_files/skills/tools_allow) are
|
|
18
|
+
# AUTHORITATIVE from the pack, so whatever left the pack leaves the agent
|
|
19
|
+
# (isolation and no drift). An in-flight turn keeps the profile it captured.
|
|
20
|
+
class PackImporter
|
|
21
|
+
def initialize(bus:, profiles:)
|
|
22
|
+
@bus = bus
|
|
23
|
+
@profiles = profiles
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# -> { agent_id:, created:, files: [names], skills: [names], tools: [names] }.
|
|
27
|
+
# Validation/lookup errors from the Commands (missing id/model, etc.) propagate
|
|
28
|
+
# (the transport maps them to 422/404).
|
|
29
|
+
def import(pack)
|
|
30
|
+
id = presence(pack.config[:id]) ||
|
|
31
|
+
(raise Insika::ValidationError, "pack missing config.id")
|
|
32
|
+
|
|
33
|
+
created = @profiles[id].nil?
|
|
34
|
+
# create_agent rejects an already-existing id; update_agent requires it to
|
|
35
|
+
# exist — the choice by presence makes the import an upsert.
|
|
36
|
+
dispatch(created ? :create_agent : :update_agent, agent_attrs(pack, id))
|
|
37
|
+
|
|
38
|
+
pack.files.each { |name, body| dispatch(:write_agent_file, { agent_id: id, file: name, content: body }) }
|
|
39
|
+
pack.skills.each { |name, body| dispatch(:write_skill, { name: name, content: body }) }
|
|
40
|
+
pack.tools.each { |defn| dispatch(:write_data_tool, defn) }
|
|
41
|
+
|
|
42
|
+
{ agent_id: id, created: created,
|
|
43
|
+
files: pack.files.keys, skills: pack.skills.keys, tools: pack.tools.map { |t| tool_name(t) } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Removes the agent (delete_agent). Does NOT delete skills/tools/files: they
|
|
47
|
+
# may be shared and the SkillStore/ToolStore are global — selective removal is
|
|
48
|
+
# operator work. NotFoundError (missing agent) propagates -> 404.
|
|
49
|
+
def delete(id)
|
|
50
|
+
dispatch(:delete_agent, { id: id })
|
|
51
|
+
{ agent_id: id, deleted: true }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# AgentProfile.build attrs: the pack manifest + the allowlists derived and
|
|
57
|
+
# AUTHORITATIVE from the pack. This way the agent only sees the skills/tools of
|
|
58
|
+
# its OWN pack (per-store isolation) and re-provisioning removes what left.
|
|
59
|
+
# - prompt_files = the pack's .md files (write_agent_file also registers;
|
|
60
|
+
# union is a no-op). Setting here makes the list authoritative (removes the ones that left).
|
|
61
|
+
# - skills = the pack's skills/ dirs (explicit allowlist; [] if none in the
|
|
62
|
+
# pack and config didn't declare any — never nil=all, which would leak
|
|
63
|
+
# skills from other stores).
|
|
64
|
+
# - tools_allow = (config.tools_allow) ∪ (the pack's tool names) — guarantees
|
|
65
|
+
# the agent can call its own data-tools (NF2).
|
|
66
|
+
# - tools_allow_groups = groups enabled by FLAG in the pack (see
|
|
67
|
+
# #enabled_groups) — the per-flag schema CUT (Phase 7, Stage E / D5): only
|
|
68
|
+
# the tools of the enabled groups (union with tools_allow) go to the model;
|
|
69
|
+
# those of disabled groups are cut BEFORE the turn (resolves the OpenClaw
|
|
70
|
+
# tool-call waste, where the flag only exists in Rails).
|
|
71
|
+
def agent_attrs(pack, id)
|
|
72
|
+
attrs = pack.config.dup
|
|
73
|
+
attrs[:id] = id
|
|
74
|
+
attrs[:prompt_files] = pack.files.keys unless pack.files.empty?
|
|
75
|
+
attrs[:skills] = pack.skills.keys if pack.skills.any? || pack.config.key?(:skills)
|
|
76
|
+
|
|
77
|
+
pack_tools = pack.tools.map { |t| tool_name(t) }
|
|
78
|
+
allow = Array(pack.config[:tools_allow]).map(&:to_s) | pack_tools
|
|
79
|
+
attrs[:tools_allow] = allow unless allow.empty? && !pack.config.key?(:tools_allow)
|
|
80
|
+
|
|
81
|
+
groups = enabled_groups(pack.config)
|
|
82
|
+
attrs[:tools_allow_groups] = groups unless groups.nil?
|
|
83
|
+
attrs
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# PER-FLAG CUT (Phase 7, Stage E / D5, STATIC pilot): derives the agent's
|
|
87
|
+
# per-group allowlist from FLAGS declared in the pack config — DATA, never a
|
|
88
|
+
# core convention (NF1). The engine doesn't know "groceries_v2"/"b2b": the pack
|
|
89
|
+
# declares which GROUPS are enabled; the flag->group mapping is the
|
|
90
|
+
# responsibility of provisioning/the pack, not the insika. Two forms (union):
|
|
91
|
+
# - `enabled_groups: ["default", "b2b"]` — explicit list of ON groups.
|
|
92
|
+
# - `flags: { "b2b" => true, "natura" => false }` — the flag key IS the group
|
|
93
|
+
# name; only the truthy ones enter (the false ones CUT the group).
|
|
94
|
+
# Neither declared -> nil (no per-group cut; old behavior).
|
|
95
|
+
# Explicit `[]` (empty enabled_groups, or all flags false) -> no group.
|
|
96
|
+
def enabled_groups(config)
|
|
97
|
+
return nil unless config.key?(:enabled_groups) || config.key?(:flags)
|
|
98
|
+
|
|
99
|
+
from_list = Array(config[:enabled_groups]).map(&:to_s)
|
|
100
|
+
from_flags = flag_groups(config[:flags])
|
|
101
|
+
(from_list | from_flags)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# { group => bool } -> [groups with a truthy value]. Accepts string|symbol keys.
|
|
105
|
+
def flag_groups(flags)
|
|
106
|
+
return [] unless flags.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
flags.filter_map { |group, on| group.to_s if truthy?(on) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def truthy?(val) = val == true || val.to_s == "true"
|
|
112
|
+
|
|
113
|
+
def tool_name(defn) = (defn[:name] || defn["name"]).to_s
|
|
114
|
+
|
|
115
|
+
def presence(str) = Insika::Coercion.presence(str)
|
|
116
|
+
|
|
117
|
+
def dispatch(type, payload)
|
|
118
|
+
@bus.dispatch(Insika::Command.build(type, payload, transport: :http))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
# Domain store for PENDING approval ACTIONS ("state as a
|
|
8
|
+
# record, not a flag"). A tool marked `approval` (Policy) creates a
|
|
9
|
+
# PendingAction and the turn goes to :waiting; the operator resolves it via ApproveAction.
|
|
10
|
+
# Durable (over an injected Insika::Store): survives a kill -9 — the operator
|
|
11
|
+
# approves after the reboot, and Recovery rehydrates the task in :waiting.
|
|
12
|
+
#
|
|
13
|
+
# Normalizes symbol→string on WRITE (the backend only guarantees round-trip of JSON
|
|
14
|
+
# types), like the other domain stores.
|
|
15
|
+
class PendingActionStore
|
|
16
|
+
include Coercion
|
|
17
|
+
|
|
18
|
+
SCOPE = "pending_actions"
|
|
19
|
+
KEY_PREFIX = "pending:"
|
|
20
|
+
|
|
21
|
+
STATUSES = %i[pending approved rejected].freeze
|
|
22
|
+
|
|
23
|
+
PendingAction = Data.define(:id, :task_id, :turn, :tool, :args,
|
|
24
|
+
:status, :requested_at, :resolved_by, :resolved_at)
|
|
25
|
+
|
|
26
|
+
def initialize(store:)
|
|
27
|
+
@store = store
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# -> PendingAction (:pending). `args` is the Hash of the tool call's arguments.
|
|
31
|
+
def create(task_id:, turn:, tool:, args: {}, id: SecureRandom.uuid)
|
|
32
|
+
record = {
|
|
33
|
+
"id" => id.to_s,
|
|
34
|
+
"task_id" => task_id.to_s,
|
|
35
|
+
"turn" => turn,
|
|
36
|
+
"tool" => tool.to_s,
|
|
37
|
+
"args" => deep_stringify(args),
|
|
38
|
+
"status" => "pending",
|
|
39
|
+
"requested_at" => timestamp,
|
|
40
|
+
"resolved_by" => nil,
|
|
41
|
+
"resolved_at" => nil
|
|
42
|
+
}
|
|
43
|
+
@store.set(SCOPE, key_for(id), record)
|
|
44
|
+
to_pending(record)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# -> PendingAction | nil
|
|
48
|
+
def find(id)
|
|
49
|
+
record = @store.get(SCOPE, key_for(id))
|
|
50
|
+
record && to_pending(record)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# -> [PendingAction] :pending for the task (recovery/UI). O(n) scan — single-node,
|
|
54
|
+
# like TaskStore#running_or_interrupted.
|
|
55
|
+
def open_for(task_id)
|
|
56
|
+
id = task_id.to_s
|
|
57
|
+
@store.list(SCOPE, KEY_PREFIX).filter_map do |key|
|
|
58
|
+
record = @store.get(SCOPE, key)
|
|
59
|
+
next if record.nil?
|
|
60
|
+
|
|
61
|
+
pa = to_pending(record)
|
|
62
|
+
pa if pa.status == :pending && pa.task_id == id
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# -> [PendingAction] every :pending across all tasks — the approvals inbox
|
|
67
|
+
# (§12 G5, Studio). Single O(n) scan (vs. open_for per task = O(n·m)); the
|
|
68
|
+
# UI resolves task context afterwards via TaskStore#find.
|
|
69
|
+
def all_open
|
|
70
|
+
@store.list(SCOPE, KEY_PREFIX).filter_map do |key|
|
|
71
|
+
record = @store.get(SCOPE, key)
|
|
72
|
+
next if record.nil?
|
|
73
|
+
|
|
74
|
+
pa = to_pending(record)
|
|
75
|
+
pa if pa.status == :pending
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# -> resolved PendingAction. Only resolves :pending: a double resolution
|
|
80
|
+
# or an invalid decision -> ValidationError; absent -> NotFoundError.
|
|
81
|
+
def resolve(id, decision:, operator: nil)
|
|
82
|
+
target = decision.to_sym
|
|
83
|
+
unless %i[approved rejected].include?(target)
|
|
84
|
+
raise Insika::ValidationError, "invalid decision: #{decision} (approved|rejected)"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
record = @store.get(SCOPE, key_for(id)) ||
|
|
88
|
+
(raise Insika::NotFoundError, "pending action not found: #{id}")
|
|
89
|
+
unless record["status"] == "pending"
|
|
90
|
+
raise Insika::ValidationError, "pending action '#{id}' already resolved (#{record["status"]})"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
record["status"] = target.to_s
|
|
94
|
+
record["resolved_by"] = operator&.to_s
|
|
95
|
+
record["resolved_at"] = timestamp
|
|
96
|
+
@store.set(SCOPE, key_for(id), record)
|
|
97
|
+
to_pending(record)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def key_for(id) = "#{KEY_PREFIX}#{id}"
|
|
103
|
+
|
|
104
|
+
def to_pending(record)
|
|
105
|
+
PendingAction.new(
|
|
106
|
+
id: record["id"],
|
|
107
|
+
task_id: record["task_id"],
|
|
108
|
+
turn: record["turn"],
|
|
109
|
+
tool: record["tool"],
|
|
110
|
+
args: record["args"],
|
|
111
|
+
status: record["status"].to_sym,
|
|
112
|
+
requested_at: record["requested_at"],
|
|
113
|
+
resolved_by: record["resolved_by"],
|
|
114
|
+
resolved_at: record["resolved_at"]
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def timestamp = Time.now.utc.iso8601
|
|
119
|
+
end
|
|
120
|
+
end
|