insika 0.2.0 → 0.7.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 +325 -17
- data/README.md +52 -11
- data/bin/insika +895 -8
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +229 -163
- data/docs/API.md +56 -0
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +111 -21
- data/docs/CONTEXT.md +132 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +75 -4
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +146 -2
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +116 -0
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +16 -10
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +65 -6
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +17 -12
- data/docs/RELEASING.md +27 -0
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +6 -6
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +79 -2
- data/docs/SKILLS.md +13 -4
- data/docs/SOAK.md +127 -0
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +220 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +115 -0
- data/docs/improve.md +20 -0
- data/docs/index.md +9 -6
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +202 -10
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +119 -14
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +105 -2
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +107 -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 +1 -1
- 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/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +57 -23
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +889 -11
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +69 -82
- data/lib/insika/dsl/server_boot.rb +92 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +186 -5
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +25 -3
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +29 -6
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +869 -76
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +198 -0
- data/lib/insika/followup_store.rb +306 -0
- 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 +17 -2
- 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/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +382 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +7 -2
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +22 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +37 -17
- 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/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +29 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +316 -0
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- 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 +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +247 -17
- data/lib/insika/server/rack_app.rb +21 -1
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +16 -0
- 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 +21 -10
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1437 -54
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +23 -23
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +308 -10
- data/lib/insika/studio/nav_icons.rb +19 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +62 -278
- data/lib/insika/studio/views/agents.erb +71 -54
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- 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 +6 -3
- 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 +106 -66
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +15 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +233 -124
- data/lib/insika/studio/views/settings.erb +41 -1
- data/lib/insika/studio/views/skills.erb +1 -2
- 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 +24 -10
- data/lib/insika/task_store.rb +21 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +47 -11
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +89 -0
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +49 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +472 -13
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +149 -3
- metadata +181 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Prompt — go live
|
|
3
|
+
parent: Reference
|
|
4
|
+
nav_order: 5
|
|
5
|
+
permalink: /go-live/
|
|
6
|
+
layout: default
|
|
7
|
+
render_with_liquid: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Take this agent to production
|
|
11
|
+
|
|
12
|
+
> **You are a coding agent** (Claude Code, Codex, Cursor, …) reading this because a
|
|
13
|
+
> developer pasted a prompt pointing here — something like *"deploy this"* or *"take
|
|
14
|
+
> it to production"*. Treat this file as a **skill**: follow the steps in order and
|
|
15
|
+
> apply the RULES literally. Production is where shortcuts become incidents.
|
|
16
|
+
|
|
17
|
+
Your job: get **one working local setup** running as **one production instance**,
|
|
18
|
+
verified end to end. The authoritative reference is
|
|
19
|
+
[`docs/DEPLOY.md`](../DEPLOY.md) (served at `GET /docs/deploy.md`); this file is the
|
|
20
|
+
ordered path through it.
|
|
21
|
+
|
|
22
|
+
## Step 0 — Gather context (silently)
|
|
23
|
+
|
|
24
|
+
- **Repo or gem?** The reference deployment is a checkout of the insika repo
|
|
25
|
+
(`Dockerfile` + `config.ru` + `railway.json` already in it). An adopter's own app
|
|
26
|
+
consumes the gem instead — then the developer's repo needs its own image; the env
|
|
27
|
+
contract below is identical.
|
|
28
|
+
- **Which platform?** Railway is the documented path. Any Docker host works; the
|
|
29
|
+
Kubernetes caveats are in [`docs/DEPLOY.md`](../DEPLOY.md) § Kubernetes.
|
|
30
|
+
- **Does it work locally?** One green `reply()` or `serve` turn first. Do not debug an
|
|
31
|
+
agent and a deployment at the same time.
|
|
32
|
+
- Read [`docs/DEPLOY.md`](../DEPLOY.md) and
|
|
33
|
+
[`docs/SECURITY.md`](../SECURITY.md) before writing anything.
|
|
34
|
+
|
|
35
|
+
## Step 1 — Mint the two secrets (RULES, not taste)
|
|
36
|
+
|
|
37
|
+
Two tokens, **two different values** — the fallback of one onto the other is a dev
|
|
38
|
+
convenience only:
|
|
39
|
+
|
|
40
|
+
| Token | Gates | Rotating it |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `ADMIN_TOKEN` | `/studio` login (the operator — just you) | safe, independent |
|
|
43
|
+
| `OPENCLAW_GATEWAY_TOKEN` | Bearer for `/v1/responses` + `/v1/agents` (your API consumers) | both sides together, same step |
|
|
44
|
+
|
|
45
|
+
Generate each: `ruby -rsecurerandom -e 'puts SecureRandom.hex(24)'`. Set them as
|
|
46
|
+
platform env vars. **Never** write either into a file, a commit, or your own output.
|
|
47
|
+
|
|
48
|
+
## Step 2 — The non-negotiable env
|
|
49
|
+
|
|
50
|
+
- **`INSIKA_DB` on a mounted volume** (the image defaults to `/data/insika.db` —
|
|
51
|
+
mount a volume at `/data`). No volume = SQLite is ephemeral and recovery resumes
|
|
52
|
+
nothing after a redeploy.
|
|
53
|
+
- **`WEB_CONCURRENCY` stays `1`.** It is a contract input, not a throughput knob:
|
|
54
|
+
N>1 without session-sticky routing in front is a guaranteed cross-session reply
|
|
55
|
+
leak, and `insika doctor` errors on it on Railway. The fix, when throughput is
|
|
56
|
+
actually needed, is [`insika-router`](../ROUTER.md) in front — not a bigger number.
|
|
57
|
+
- **Provider key** (`DEEPSEEK_API_KEY` for the demo provider) — without it the engine
|
|
58
|
+
still boots (`/up` green) but every turn fails until it is configured.
|
|
59
|
+
- **`INSIKA_EGRESS_HOSTS`** = exactly the hosts your data-tools call. A backend on the
|
|
60
|
+
developer's machine gets a public **https tunnel** + its host in this list — never
|
|
61
|
+
`INSIKA_EGRESS_ALLOW_HTTP`/`_ALLOW_PRIVATE` in cloud.
|
|
62
|
+
- On Railway also **`RAILWAY_DEPLOYMENT_DRAINING_SECONDS=30`**: the platform default
|
|
63
|
+
is 0 — SIGKILL right after SIGTERM — which cancels the graceful drain entirely.
|
|
64
|
+
|
|
65
|
+
## Step 3 — Deploy
|
|
66
|
+
|
|
67
|
+
Railway (repo path — `railway.json` already sets builder, start command, `/up`
|
|
68
|
+
healthcheck, restart policy):
|
|
69
|
+
|
|
70
|
+
1. Create the project/service from the repo (builder = Dockerfile).
|
|
71
|
+
2. Mount the volume at `/data`.
|
|
72
|
+
3. Set the vars from Steps 1–2.
|
|
73
|
+
4. Deploy; the healthcheck must go green on `/up`.
|
|
74
|
+
|
|
75
|
+
Any Docker host, same contract:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
docker build -t insika .
|
|
79
|
+
docker run -p 9292:9292 -v insika-data:/data \
|
|
80
|
+
-e DEEPSEEK_API_KEY=... -e ADMIN_TOKEN=... -e OPENCLAW_GATEWAY_TOKEN=... \
|
|
81
|
+
insika
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Step 4 — Prove it with ONE real turn
|
|
85
|
+
|
|
86
|
+
In order, each with evidence:
|
|
87
|
+
|
|
88
|
+
1. `curl https://<host>/up` → `{"status":"ok"}`.
|
|
89
|
+
2. `bin/insika doctor` against the deployed volume (or via the platform's shell) —
|
|
90
|
+
relay its findings verbatim; fix errors before continuing.
|
|
91
|
+
3. One authenticated turn:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
curl -N https://<host>/v1/responses \
|
|
95
|
+
-H "Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN" \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{"model":"<agent-id>","user":"go-live-check","input":"hello"}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The reply must be real model output. 401 → token mismatch (Step 1); a provider error
|
|
101
|
+
→ key/model id (Step 2); anything else → stop and diagnose with
|
|
102
|
+
[`docs/prompts/DIAGNOSE-TURN.md`](DIAGNOSE-TURN.md) before touching config.
|
|
103
|
+
|
|
104
|
+
4. Log in to `/studio` with the new `ADMIN_TOKEN` and find the go-live-check session.
|
|
105
|
+
|
|
106
|
+
## Step 5 — Close the total-loss hole (Litestream)
|
|
107
|
+
|
|
108
|
+
A single volume is the one point of total loss. Enable continuous replication by env
|
|
109
|
+
(off by default, zero code): set `LITESTREAM_REPLICA_URL` + credentials per
|
|
110
|
+
[`docs/DEPLOY.md`](../DEPLOY.md) § Backup / DR. Then **run the restore drill** — an
|
|
111
|
+
untested backup does not count:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
scripts/litestream-restore-drill.sh # local proof of the mechanism, or the
|
|
115
|
+
# production drill in DEPLOY.md § Restore drill
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If the developer declines Litestream, record that as an explicit accepted risk in
|
|
119
|
+
your report — do not silently skip it.
|
|
120
|
+
|
|
121
|
+
## Step 6 — Self-check
|
|
122
|
+
|
|
123
|
+
- [ ] `/up` green, `doctor` clean, one real authenticated turn with model output.
|
|
124
|
+
- [ ] Two distinct tokens, both only in platform env; nothing secret in git or logs.
|
|
125
|
+
- [ ] Volume mounted; `WEB_CONCURRENCY=1`; drain buffer set (Railway).
|
|
126
|
+
- [ ] Egress allowlist names only the hosts the tools actually call.
|
|
127
|
+
- [ ] Litestream on **and** a restore exercised — or the risk explicitly accepted.
|
|
128
|
+
|
|
129
|
+
## Hard constraints
|
|
130
|
+
|
|
131
|
+
- **Never raise `WEB_CONCURRENCY` to "fix" throughput.** The failure it causes is a
|
|
132
|
+
reply delivered to the wrong customer — read
|
|
133
|
+
[`docs/DEPLOY.md`](../DEPLOY.md) § The process model before proposing any scaling.
|
|
134
|
+
- **`_ALLOW_HTTP`/`_ALLOW_PRIVATE` never in cloud.** They exist for fully-local loops.
|
|
135
|
+
- **The onboarding surface (`/start.md`, `/docs`) is opt-in in production**
|
|
136
|
+
(`INSIKA_ONBOARDING=1`) — leaving it off is the default posture, not a bug.
|
|
137
|
+
- **Report every deviation.** A var you had to add, a check that failed and was
|
|
138
|
+
worked around, a step the platform made impossible — findings, not noise.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Prompt — run every example
|
|
3
|
+
parent: Reference
|
|
4
|
+
nav_order: 2
|
|
5
|
+
permalink: /run-examples/
|
|
6
|
+
layout: default
|
|
7
|
+
render_with_liquid: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Run every example
|
|
11
|
+
|
|
12
|
+
> **You are a coding agent** (Claude Code, Codex, Cursor, …) reading this because a
|
|
13
|
+
> developer pasted a prompt pointing here. Treat this file as a **skill**: follow the
|
|
14
|
+
> steps in order and apply the RULES literally. Do not improvise beyond them.
|
|
15
|
+
|
|
16
|
+
Your job: get every runnable example under `examples/` running — one at a time — and
|
|
17
|
+
explain to the developer what each one demonstrates. The authoritative list (and the
|
|
18
|
+
one-line capability per example) is
|
|
19
|
+
[`examples/README.md`](https://github.com/guizaols/insika/tree/main/examples/README.md)
|
|
20
|
+
(`examples/README.md` when the repo is checked out). Read it first.
|
|
21
|
+
|
|
22
|
+
## Step 0 — Gather context (do this first, silently)
|
|
23
|
+
|
|
24
|
+
RULES — verify, do not assume:
|
|
25
|
+
|
|
26
|
+
- **Ruby ≥ 3.3.** Run `ruby -v`. If lower, stop and tell the developer; do not try to
|
|
27
|
+
upgrade Ruby for them.
|
|
28
|
+
- **Insika must be loadable** — `require "insika"` (installed gem) or the checked-out
|
|
29
|
+
repo's bundle. Do not copy source files around to "fix" a missing install.
|
|
30
|
+
- **A provider key comes from the environment** (`DEEPSEEK_API_KEY` for the demo).
|
|
31
|
+
None is set → **ask the developer**; never invent or hard-code one.
|
|
32
|
+
- **Read each example's own `README.md` before running it.** Some need more than one
|
|
33
|
+
terminal or extra env vars; the README is the contract.
|
|
34
|
+
|
|
35
|
+
## Step 1 — Run in this order, ONE at a time
|
|
36
|
+
|
|
37
|
+
| # | Example | Command | Note |
|
|
38
|
+
|---|---------|---------|------|
|
|
39
|
+
| 1 | hello-agent | `ruby examples/hello-agent/hello.rb` | smallest agent; one turn |
|
|
40
|
+
| 2 | data-tool | `ruby examples/data-tool/currency_agent.rb` | declarative HTTP tool + the egress guard |
|
|
41
|
+
| 3 | skills | `ruby examples/skills/skill_agent.rb` | progressive skill loading |
|
|
42
|
+
| 4 | memory | `ruby examples/memory/memory_agent.rb` | cross-session `remember` |
|
|
43
|
+
| 5 | guardrails | `ruby examples/guardrails/guarded_agent.rb` | content-safety guardrails |
|
|
44
|
+
| 6 | agentic-workflows | `ruby examples/agentic-workflows/sequential.rb` | then routing/parallel/delegation/evaluator |
|
|
45
|
+
| 7 | scheduled-report | `ruby examples/scheduled-report/report_agent.rb` | runs one report turn inline; add `--serve` only if asked |
|
|
46
|
+
| 8 | relay-channel | see its README | TWO processes + env vars; skip unless asked |
|
|
47
|
+
|
|
48
|
+
For each one: read its README → run → quote what it printed → explain in ≤ 3 lines
|
|
49
|
+
what capability it demonstrated.
|
|
50
|
+
|
|
51
|
+
Skip unless the developer asks: `insika-code/` (a full deployment, not a one-file
|
|
52
|
+
script), `quickstart.rb` (hello-agent already covers it), and anything in `examples/`
|
|
53
|
+
the table above does not list.
|
|
54
|
+
|
|
55
|
+
## Step 2 — When one fails
|
|
56
|
+
|
|
57
|
+
- **Auth or model error** → stop that example, report the exact error, ask for a valid
|
|
58
|
+
key/model id. Never retry with a guessed id.
|
|
59
|
+
- **A cause you can read from the output** (missing env var, port already bound…) →
|
|
60
|
+
say so and fix only with the developer's OK.
|
|
61
|
+
- **Never edit an example to make it pass silently.** An example that needed a change
|
|
62
|
+
to run is a finding, not noise.
|
|
63
|
+
|
|
64
|
+
## Step 3 — Self-check before you report done
|
|
65
|
+
|
|
66
|
+
- [ ] Every example above either printed real model output or was reported blocked,
|
|
67
|
+
with the exact blocker.
|
|
68
|
+
- [ ] No provider key written into any file; no invented model ids.
|
|
69
|
+
- [ ] Each example got its ≤ 3-line "what this demonstrates".
|
|
70
|
+
- [ ] Nothing was edited to make a failure disappear.
|
data/docs/reference.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reference
|
|
3
|
+
nav_order: 8
|
|
4
|
+
has_children: true
|
|
5
|
+
permalink: /reference/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reference
|
|
9
|
+
|
|
10
|
+
The removability map, plus the paste-prompts that hand a journey to a coding
|
|
11
|
+
agent. A running instance serves each prompt at `GET /docs/<name>.md`, so you can
|
|
12
|
+
point Claude Code, Codex or Cursor at the URL instead of pasting the text.
|
|
13
|
+
|
|
14
|
+
- **[The domain-free core](domain.md)** — what ships in the gem, what a deployment declares, and how to clear it.
|
|
15
|
+
- **[Prompt — run every example](prompts/RUN-EXAMPLES.md)** — run `examples/` one at a time and explain what each proves.
|
|
16
|
+
- **[Prompt — add a tool or skill](prompts/ADD-TOOL.md)** — pick the right kind, wire the allowlist, prove it with one turn.
|
|
17
|
+
- **[Prompt — diagnose a failed turn](prompts/DIAGNOSE-TURN.md)** — symptom to mechanism, then fix one thing.
|
|
18
|
+
- **[Prompt — go live](prompts/GO-LIVE.md)** — tokens, volume, deploy, one authenticated turn, and the backup hole.
|
|
19
|
+
{: .card-grid }
|
data/docs/ship.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Ship it
|
|
3
|
-
nav_order:
|
|
3
|
+
nav_order: 5
|
|
4
4
|
has_children: true
|
|
5
5
|
permalink: /ship/
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Ship it
|
|
9
9
|
|
|
10
|
-
What stands between your agent and the open internet, and how to put it on a
|
|
10
|
+
What stands between your agent and the open internet, and how to put it on a
|
|
11
|
+
server without losing a turn to a restart.
|
|
12
|
+
|
|
13
|
+
- **[Security](SECURITY.md)** — guardrails, egress, approvals, secrets, and the privacy obligations that come with memory.
|
|
14
|
+
- **[Sandbox](SANDBOX.md)** — confined execution for tool code you did not write.
|
|
15
|
+
- **[Deploy](DEPLOY.md)** — the container, the durable volume, the process model, and the full environment table.
|
|
16
|
+
- **[Router](ROUTER.md)** — the session-sticky proxy that lets you run more than one worker.
|
|
17
|
+
- **[Releasing](RELEASING.md)** — how the gem is cut, and the install proof that runs before it is published.
|
|
18
|
+
{: .card-grid }
|
data/docs/start-here.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Start here
|
|
3
|
+
nav_order: 2
|
|
4
|
+
has_children: true
|
|
5
|
+
permalink: /start-here/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Start here
|
|
9
|
+
|
|
10
|
+
Four pages, in order: what problem the runtime solves, how to get one running on
|
|
11
|
+
your machine, what actually happens inside a turn, and how to fill an empty
|
|
12
|
+
install with enough data to see every loop working.
|
|
13
|
+
|
|
14
|
+
- **[Why Insika](WHY.md)** — a runtime instead of a hand-rolled loop, an assembled framework, or a hosted gateway.
|
|
15
|
+
- **[Running locally](RUNNING-LOCAL.md)** — boot the engine, open the control UI, point a Responses client at it.
|
|
16
|
+
- **[Architecture](ARCHITECTURE.md)** — the turn pipeline, the tool-loop, checkpoint recovery, the concurrency model.
|
|
17
|
+
- **[Demo data](DEMO.md)** — seed a deployment so funnels, refinement and approvals have something to show.
|
|
18
|
+
{: .card-grid }
|
data/lib/insika/agent_profile.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "coercion"
|
|
4
|
+
require_relative "tool_definition"
|
|
4
5
|
|
|
5
6
|
module Insika
|
|
6
7
|
# Single point of per-agent policy.
|
|
@@ -63,6 +64,14 @@ module Insika
|
|
|
63
64
|
# a context provider injecting volatile content into
|
|
64
65
|
# :system turns every turn into a paid cache WRITE with
|
|
65
66
|
# no read hit. Enable only for stable-system agents.
|
|
67
|
+
:tool_persistence, # the engine's "Tool discipline" block in the system
|
|
68
|
+
# prompt (retry weak/empty tool results with a different
|
|
69
|
+
# approach before giving up). THE ONE OPT-OUT FIELD:
|
|
70
|
+
# nil/true = ON (the proven default — every reference
|
|
71
|
+
# harness ships it), false = OFF. Deliberately inverted
|
|
72
|
+
# from the opt-in fields above: the exception here is
|
|
73
|
+
# turning the good behavior OFF, so that is what an
|
|
74
|
+
# operator declares. Read by Context::Providers::Prompt.
|
|
66
75
|
:tool_output_compression, # MECHANICAL tool-result dedupe in the replayed
|
|
67
76
|
# history (A3/C3): nil/false = OFF (parity); true = ON.
|
|
68
77
|
# Same opt-in as `memory`. When ON, the history the
|
|
@@ -103,6 +112,14 @@ module Insika
|
|
|
103
112
|
# breaker_open / delivery_failed events are POSTed to
|
|
104
113
|
# the URL as JSON (outbox + claim, at-most-once).
|
|
105
114
|
# nil/absent = no webhook (parity).
|
|
115
|
+
:routes, # intent routing (WS4): { "route" => "description" or a
|
|
116
|
+
# Hash with description/delegate/stuck/message,
|
|
117
|
+
# "default" => route, "model" => cheap classifier }.
|
|
118
|
+
# When present, the message is classified BEFORE the
|
|
119
|
+
# ask with the cheap model; the route lands on the
|
|
120
|
+
# turn (state.route + :route_classified) and may
|
|
121
|
+
# delegate to an existing agent or end the turn :stuck
|
|
122
|
+
# (WS5). nil/absent = no routing (parity).
|
|
106
123
|
:stuck_signal, # the agent may signal it cannot proceed (WS5):
|
|
107
124
|
# nil/false = OFF (parity — the signal_stuck system
|
|
108
125
|
# tool is not wired); true = ON (the model may call
|
|
@@ -111,6 +128,25 @@ module Insika
|
|
|
111
128
|
# event the consumer acts on). Same opt-in as
|
|
112
129
|
# `memory`. What "stuck" MEANS is the consumer's call
|
|
113
130
|
# (escalation via CRM/operator), never the engine's.
|
|
131
|
+
:stt_prompt, # STT vocabulary hint (WS9): domain words
|
|
132
|
+
# (product names, brand terms) the transcriber should
|
|
133
|
+
# expect on this agent's voice notes — passed straight
|
|
134
|
+
# through to the Whisper-family provider's `prompt:`.
|
|
135
|
+
# nil/absent = the deployment default (INSIKA_STT_PROMPT
|
|
136
|
+
# env) or nothing. OPERATOR config, never customer input.
|
|
137
|
+
:outputs, # generated-media output policy (WS9, saída):
|
|
138
|
+
# { "image" => { "model" => …, "size" => "1024x1024" },
|
|
139
|
+
# "tts" => { "model" => "tts-1", "voice" => "alloy",
|
|
140
|
+
# "format" => "mp3" } }. THE AGENT'S HALF of the
|
|
141
|
+
# media-output gate — nil/absent = the agent never
|
|
142
|
+
# generates media (opt-in like `capabilities`, do NOT
|
|
143
|
+
# "fix" to nil = all). The other half is the CHANNEL'S:
|
|
144
|
+
# the request must declare it can receive the media
|
|
145
|
+
# (`channel.capabilities` — "image_output" /
|
|
146
|
+
# "audio_output"); only with BOTH does the model see
|
|
147
|
+
# the generate_image/tts tools (the abstraction admits
|
|
148
|
+
# only what leaks). Generated media rides the turn's
|
|
149
|
+
# `output_parts` in the envelope, never the answer text.
|
|
114
150
|
:model_policy, # governance of WHICH models the agent may use:
|
|
115
151
|
# { "allow" => [refs] }. nil = NO fence (all models —
|
|
116
152
|
# parity). Enforced on the RESOLVED model (ModelResolver).
|
|
@@ -162,10 +198,102 @@ module Insika
|
|
|
162
198
|
# (WhatsApp) puts the deliberation in front of a
|
|
163
199
|
# customer; that is the operator's call to make, not a
|
|
164
200
|
# default to inherit.
|
|
165
|
-
:
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
201
|
+
:grounding, # the pack's grounding policy —
|
|
202
|
+
# { "mode" => "flag"|"enforce"|"off",
|
|
203
|
+
# "matcher" => { "sku" => …,
|
|
204
|
+
# "name_keys" => [...] } }. OPT-IN:
|
|
205
|
+
# nil/absent = OFF (parity, zero allocations).
|
|
206
|
+
# Deep-stringified like the other hashes.
|
|
207
|
+
:metadata, # free-form agent metadata, stable per agent
|
|
208
|
+
# (from the pack `agent.config.json`). Home of the `store_id`
|
|
209
|
+
# that becomes turn context (ctx.store_id).
|
|
210
|
+
# It is NOT a policy — never decides security. {} = absent.
|
|
211
|
+
:briefing_fields, # the per-session working-state schema this agent
|
|
212
|
+
# keeps and asks for: a flat [String] of
|
|
213
|
+
# field names the pack declares. []/nil/absent = the
|
|
214
|
+
# feature is OFF (no provider output, no tools — visibly
|
|
215
|
+
# removable). Names are engine-owned store keys
|
|
216
|
+
# and tool text, so they are validated against NAME_RE at
|
|
217
|
+
# build time. Data, never a policy: the engine owns the
|
|
218
|
+
# briefing object, the pack owns the fields.
|
|
219
|
+
:funnel, # the outcome funnel declaration — pack
|
|
220
|
+
# data, exactly like budget/reliability:
|
|
221
|
+
# { "stages" => ["greeted", "qualified", "cart", "paid"],
|
|
222
|
+
# "advance_on" => { "pix_paid" => "paid", … },
|
|
223
|
+
# "primary" => "paid", "attribution_window" => "72h" }.
|
|
224
|
+
# The ENGINE never hard-codes a stage name: the fold,
|
|
225
|
+
# the doctor and the Studio read this declaration (D1).
|
|
226
|
+
# nil/absent = no funnel (parity — nothing folds).
|
|
227
|
+
# Deep-stringified like the other free-form hashes;
|
|
228
|
+
# shape-validated by FunnelDeclaration, never here (D8).
|
|
229
|
+
:followup, # the follow-up declaration — pack data,
|
|
230
|
+
# exactly like budget/funnel:
|
|
231
|
+
# { "arm" => "schedule",
|
|
232
|
+
# "policy" => { "quiet_hours" => { "timezone" => "…",
|
|
233
|
+
# "start" => "21:30", "end" => "09:00" },
|
|
234
|
+
# "max_frequency" => "2/24h",
|
|
235
|
+
# "cancel_keywords" => ["não quero mais contato"],
|
|
236
|
+
# "silence_after_sends" => 3 } }.
|
|
237
|
+
# The engine OWNS the firing, never a policy value (D1);
|
|
238
|
+
# shape-validated by FollowupPolicy, never here (D9).
|
|
239
|
+
# nil/absent = the feature is off (parity).
|
|
240
|
+
# Deep-stringified like the other free-form hashes.
|
|
241
|
+
:distill, # the session-distillation declaration — pack
|
|
242
|
+
# data, exactly like refinement/followup:
|
|
243
|
+
# { "enabled" => bool, "prompt" => "<pack-authored markdown
|
|
244
|
+
# — what counts as a fact for this store>",
|
|
245
|
+
# "model" => "<ref — absent = the platform utility_model>",
|
|
246
|
+
# "idle_hours" => 6, "min_messages" => 3,
|
|
247
|
+
# "max_proposals" => 10 }.
|
|
248
|
+
# The ENGINE assembles the scope from the session; the
|
|
249
|
+
# model only names facts (D1). nil/absent = the feature is
|
|
250
|
+
# off (parity, byte-identical engine). Shape-validated by
|
|
251
|
+
# the command/engine, never here (the refinement precedent).
|
|
252
|
+
# Deep-stringified like the other free-form hashes.
|
|
253
|
+
:harvest, # the gated-harvest declaration — pack data,
|
|
254
|
+
# exactly like refinement/distill:
|
|
255
|
+
# { "enabled" => bool,
|
|
256
|
+
# "negative_list" => [ { "rule" => "…", "pattern" => "…",
|
|
257
|
+
# "note" => "…" } ],
|
|
258
|
+
# "miner" => { "model" => "<ref — absent = the platform
|
|
259
|
+
# utility_model>", "window" => { "last_sessions" => N },
|
|
260
|
+
# "max_proposals" => N, "budget" => { "tokens" => N } },
|
|
261
|
+
# "idle_hours" => 24, "min_messages" => 3 }.
|
|
262
|
+
# The ENGINE mines (reads sessions, asks the miner, filters
|
|
263
|
+
# through the negative list + grounding), never authors a
|
|
264
|
+
# rule (D4). nil/absent = the loop is off (parity).
|
|
265
|
+
# Shape-validated by the command/engine/doctor, never here.
|
|
266
|
+
# Deep-stringified like the other free-form hashes.
|
|
267
|
+
:knowledge, # the post-turn learning declaration — pack
|
|
268
|
+
# data, exactly like distill/harvest:
|
|
269
|
+
# { "extract" => true, "retrieve" => true,
|
|
270
|
+
# "model" => "<ref — absent = the platform
|
|
271
|
+
# utility_model>", "top_k" => 5,
|
|
272
|
+
# "index" => "scan", "types" => ["fact", …] }.
|
|
273
|
+
# The ENGINE extracts concepts after the turn and
|
|
274
|
+
# stamps their provenance/confidence/sources;
|
|
275
|
+
# the model only names concepts (same D1
|
|
276
|
+
# discipline as distill). nil/absent = the
|
|
277
|
+
# loop is off (parity). Shape-validated by
|
|
278
|
+
# the extractor/doctor, never here.
|
|
279
|
+
# Deep-stringified like the other free-form hashes.
|
|
280
|
+
:schedules # the recurring-schedule declarations — pack
|
|
281
|
+
# data, exactly like followup/distill:
|
|
282
|
+
# [ { "id" => "daily_report",
|
|
283
|
+
# "cron" | "every" => …,
|
|
284
|
+
# "tz" => "America/Sao_Paulo",
|
|
285
|
+
# "message" => "<the synthetic inbound>",
|
|
286
|
+
# "session_mode" => "new"|"fixed",
|
|
287
|
+
# "overrides" => { "turn_timeout" => N,
|
|
288
|
+
# "max_tool_calls" => N,
|
|
289
|
+
# "model" => … },
|
|
290
|
+
# "enabled" => bool }, … ].
|
|
291
|
+
# The ENGINE owns the firing (the
|
|
292
|
+
# ScheduleEngine, the tick's duty); the
|
|
293
|
+
# store rows are declared-derived.
|
|
294
|
+
# Shape-validated by Insika::Schedule,
|
|
295
|
+
# never here. nil/empty = the feature is
|
|
296
|
+
# off for that agent (parity).
|
|
169
297
|
)
|
|
170
298
|
|
|
171
299
|
# Reopened class (not a Data.define block): a constant assigned inside
|
|
@@ -193,10 +321,12 @@ module Insika
|
|
|
193
321
|
skills_eager: nil, context_providers: nil, workflows_allow: nil,
|
|
194
322
|
policies: [], prompt_refs: [], limits: {}, approvals_required: nil,
|
|
195
323
|
capabilities: nil, subagents: nil, tools_deferred: nil, memory: nil,
|
|
196
|
-
prompt_caching: nil, tool_output_compression: nil,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
324
|
+
prompt_caching: nil, tool_persistence: nil, tool_output_compression: nil,
|
|
325
|
+
params: {}, model_policy: nil, guardrails: nil, sandbox: nil,
|
|
326
|
+
refinement: nil, capabilities_declared: nil, edge_stream: nil, metadata: {},
|
|
327
|
+
budget: nil, reliability: nil, alerts: nil, routes: nil, stuck_signal: nil,
|
|
328
|
+
outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil,
|
|
329
|
+
followup: nil, distill: nil, harvest: nil, knowledge: nil, schedules: nil)
|
|
200
330
|
new(
|
|
201
331
|
id: id, model: model, provider: provider, base_prompt: base_prompt,
|
|
202
332
|
prompt_files: Array(prompt_files), tools_allow: tools_allow,
|
|
@@ -210,7 +340,8 @@ module Insika
|
|
|
210
340
|
# readers get a clean [] and the ChatBuilder gate (present? => wire) is stable.
|
|
211
341
|
subagents: subagents.nil? ? nil : Array(subagents).map(&:to_s),
|
|
212
342
|
tools_deferred: tools_deferred, memory: memory,
|
|
213
|
-
prompt_caching: prompt_caching,
|
|
343
|
+
prompt_caching: prompt_caching, tool_persistence: tool_persistence,
|
|
344
|
+
tool_output_compression: tool_output_compression,
|
|
214
345
|
# The free-form hashes arrive with symbol keys (internal build) OR string
|
|
215
346
|
# keys (StoredProfileSource JSON round-trip). Normalize to string keys ONCE
|
|
216
347
|
# here — the single front door every profile passes through — so no reader
|
|
@@ -228,10 +359,71 @@ module Insika
|
|
|
228
359
|
budget: Coercion.deep_stringify(budget),
|
|
229
360
|
reliability: Coercion.deep_stringify(reliability),
|
|
230
361
|
alerts: Coercion.deep_stringify(alerts),
|
|
231
|
-
|
|
362
|
+
routes: Coercion.deep_stringify(routes),
|
|
363
|
+
stuck_signal: stuck_signal,
|
|
364
|
+
outputs: Coercion.deep_stringify(outputs),
|
|
365
|
+
# plain vocabulary string, like `base_prompt` — no deep_stringify (not
|
|
366
|
+
# a Hash/Array). "" round-trips as nil (Coercion.presence).
|
|
367
|
+
stt_prompt: Coercion.presence(stt_prompt),
|
|
368
|
+
# Flat [String] — same discipline as capabilities_declared: a
|
|
369
|
+
# symbol/string mix would be a silent miss in the provider's known-set.
|
|
370
|
+
briefing_fields: normalize_briefing_fields(briefing_fields),
|
|
371
|
+
# grounding is profile DATA, deep-stringified like the other
|
|
372
|
+
# free-form hashes; parsed into a Grounding per turn by the validator/
|
|
373
|
+
# enforcer. nil = off (parity).
|
|
374
|
+
grounding: Coercion.deep_stringify(grounding),
|
|
375
|
+
# funnel is profile DATA, deep-stringified like the other
|
|
376
|
+
# free-form hashes; parsed into a FunnelDeclaration by the fold/doctor/
|
|
377
|
+
# Studio (shape-validated THERE, never here — D8). nil = no funnel (parity).
|
|
378
|
+
funnel: Coercion.deep_stringify(funnel),
|
|
379
|
+
# followup is profile DATA, deep-stringified like the other
|
|
380
|
+
# free-form hashes; parsed into a FollowupPolicy by the tool/engine/
|
|
381
|
+
# doctor/Studio (shape-validated THERE, never here — D9). nil = off (parity).
|
|
382
|
+
followup: Coercion.deep_stringify(followup),
|
|
383
|
+
# distill is profile DATA, deep-stringified like the other
|
|
384
|
+
# free-form hashes; shape-validated by the command/engine/doctor
|
|
385
|
+
# (never here — the refinement precedent). nil = off (parity).
|
|
386
|
+
distill: Coercion.deep_stringify(distill),
|
|
387
|
+
# harvest is profile DATA, deep-stringified like the other
|
|
388
|
+
# free-form hashes; shape-validated by the command/engine/doctor
|
|
389
|
+
# (never here — the refinement precedent). nil = off (parity).
|
|
390
|
+
harvest: Coercion.deep_stringify(harvest),
|
|
391
|
+
# knowledge is profile DATA, deep-stringified like the other
|
|
392
|
+
# free-form hashes; shape-validated by the extractor/doctor
|
|
393
|
+
# (never here — the refinement precedent). nil = off (parity).
|
|
394
|
+
knowledge: Coercion.deep_stringify(knowledge),
|
|
395
|
+
# schedules is profile DATA, deep-stringified like the other
|
|
396
|
+
# free-form hashes (an ARRAY of declarations); parsed into
|
|
397
|
+
# Insika::Schedule entries by the engine/doctor/Studio (shape-validated
|
|
398
|
+
# THERE, never here). nil/[] = the feature is off (parity).
|
|
399
|
+
schedules: normalize_schedules(schedules)
|
|
232
400
|
)
|
|
233
401
|
end
|
|
234
402
|
|
|
403
|
+
# nil/absent -> nil; a single Hash -> [Hash]; else an Array of Hashes —
|
|
404
|
+
# deep-stringified so JSON round-trips stay stable.
|
|
405
|
+
def self.normalize_schedules(list)
|
|
406
|
+
return nil if list.nil?
|
|
407
|
+
|
|
408
|
+
entries = list.is_a?(Hash) ? [list] : Array(list)
|
|
409
|
+
entries.empty? ? nil : Coercion.deep_stringify(entries)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# nil -> []; strings; trim + drop empties + uniq (stable order); every name
|
|
413
|
+
# must match ToolDefinition::NAME_RE (\A[a-z][a-z0-9_]*\z) or it is a
|
|
414
|
+
# ValidationError at build time — the names become tool-description text,
|
|
415
|
+
# store keys and context-block lines, so "size ok" or "tamanho do cliente"
|
|
416
|
+
# is refused here, not corrupted later.
|
|
417
|
+
def self.normalize_briefing_fields(list)
|
|
418
|
+
names = Array(list).map { |f| f.to_s.strip }.reject(&:empty?).uniq
|
|
419
|
+
bad = names.reject { |n| ToolDefinition::NAME_RE.match?(n) }
|
|
420
|
+
unless bad.empty?
|
|
421
|
+
raise Insika::ValidationError,
|
|
422
|
+
"briefing_fields must match #{ToolDefinition::NAME_RE.inspect}: #{bad.join(', ')}"
|
|
423
|
+
end
|
|
424
|
+
names
|
|
425
|
+
end
|
|
426
|
+
|
|
235
427
|
# opt-in for an optional tool = being in the agent's allow list.
|
|
236
428
|
def tool_opted_in?(name)
|
|
237
429
|
Array(tools_allow).include?(name)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
# The signed-link half of the artifact serving surface. The signing key
|
|
8
|
+
# lives in the environment (INSIKA_ARTIFACT_SIGNING_KEY), never in a store:
|
|
9
|
+
# HMAC-SHA256 over (id, expiry), verified in constant time on the serve
|
|
10
|
+
# path. Without the key, only the authenticated Studio URL exists.
|
|
11
|
+
#
|
|
12
|
+
# The token is deterministic for a (id, expiry) pair — no nonce, on purpose:
|
|
13
|
+
# a rotated key invalidates every outstanding link, which is the documented
|
|
14
|
+
# rotation behavior (an artifact link is short-lived by TTL, not by
|
|
15
|
+
# unguessability of a single-use nonce).
|
|
16
|
+
module ArtifactSigning
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# The route's path shapes — the ONE place the URL grammar lives, shared
|
|
20
|
+
# by the tool (which hands URLs to the model) and the route (which serves
|
|
21
|
+
# them).
|
|
22
|
+
AUTHENTICATED_PATH = "/studio/artifacts/%{id}/content"
|
|
23
|
+
SIGNED_PATH = "/studio/artifacts/s/%{id}?exp=%{exp}&sig=%{sig}"
|
|
24
|
+
|
|
25
|
+
# -> hex token (64 chars) | nil when the key is blank (no signed surface).
|
|
26
|
+
def sign(id:, expires_at:, key:)
|
|
27
|
+
key = key.to_s
|
|
28
|
+
return nil if key.empty?
|
|
29
|
+
|
|
30
|
+
OpenSSL::HMAC.hexdigest("SHA256", key, payload(id, expires_at))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# -> bool. Re-signs the (id, exp) pair the route extracted from the URL
|
|
34
|
+
# and compares in constant time; an expired link or a blank key/token is
|
|
35
|
+
# false (the route 404s — no oracle). Expiry is inclusive: a link at its
|
|
36
|
+
# exact `expires_at` still verifies.
|
|
37
|
+
def valid?(id:, token:, key:, exp:, now: Time.now.utc)
|
|
38
|
+
token = token.to_s
|
|
39
|
+
key = key.to_s
|
|
40
|
+
return false if key.empty? || token.empty?
|
|
41
|
+
|
|
42
|
+
expected = sign(id: id.to_s, expires_at: exp, key: key)
|
|
43
|
+
return false unless expected && secure_compare(expected, token)
|
|
44
|
+
|
|
45
|
+
exp_time = exp.is_a?(Time) ? exp.to_time.utc : Time.iso8601(exp.to_s).utc
|
|
46
|
+
now.to_time <= exp_time
|
|
47
|
+
rescue ArgumentError, TypeError
|
|
48
|
+
false # a malformed exp (or a time that never parses) is an invalid link
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# -> the artifact's shareable URL. With a key + ttl: the signed link
|
|
52
|
+
# (shares OUTSIDE the Studio, expires). Without: the authenticated Studio
|
|
53
|
+
# content URL. An empty base yields the relative path — still openable in
|
|
54
|
+
# the Studio, useless on a channel (the doc says exactly that).
|
|
55
|
+
def url_for(id:, base: nil, key: nil, ttl: nil, now: Time.now.utc)
|
|
56
|
+
base = base.to_s.sub(%r{/\z}, "")
|
|
57
|
+
if key && key.to_s.length.positive? && ttl && ttl.to_i.positive?
|
|
58
|
+
exp = (now.to_time + ttl.to_i).utc.iso8601
|
|
59
|
+
sig = sign(id: id.to_s, expires_at: exp, key: key)
|
|
60
|
+
"#{base}#{format(SIGNED_PATH, id: id, exp: exp, sig: sig)}"
|
|
61
|
+
else
|
|
62
|
+
"#{base}#{format(AUTHENTICATED_PATH, id: id)}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Constant-time comparison of two hex strings. Length-independent compare
|
|
67
|
+
# is fine here: the token length is public (fixed by the algorithm).
|
|
68
|
+
def secure_compare(a, b)
|
|
69
|
+
return false unless a.bytesize == b.bytesize
|
|
70
|
+
|
|
71
|
+
a.bytes.zip(b.bytes).reduce(0) { |acc, (x, y)| acc | (x ^ y) }.zero?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The signed payload: id + expiry — both are what the route must not let
|
|
75
|
+
# an attacker change. The id is the store key; the expiry bounds the link.
|
|
76
|
+
# Accepts a Time or an ISO8601 String (the route passes the query param).
|
|
77
|
+
def payload(id, expires_at)
|
|
78
|
+
time = expires_at.is_a?(Time) ? expires_at.utc : Time.iso8601(expires_at.to_s).utc
|
|
79
|
+
"#{id}:#{time.iso8601}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|