protege 0.1.0.alpha.2 → 0.1.0.alpha.7
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/MIT-LICENSE +21 -0
- data/Rakefile +1 -3
- data/app/assets/builds/protege.css +76 -83
- data/app/controllers/concerns/protege/agent_scoped.rb +22 -0
- data/app/controllers/concerns/protege/agent_toolkit_scoped.rb +22 -0
- data/app/controllers/protege/access_rules_controller.rb +13 -13
- data/app/controllers/protege/agent_toolkit_rules_controller.rb +62 -0
- data/app/controllers/protege/agent_toolkits_controller.rb +44 -0
- data/app/controllers/protege/agents/agent_toolkits_controller.rb +65 -0
- data/app/controllers/protege/agents_controller.rb +118 -0
- data/app/controllers/protege/archives_controller.rb +18 -18
- data/app/controllers/protege/home_controller.rb +1 -1
- data/app/controllers/protege/messages_controller.rb +1 -1
- data/app/controllers/protege/replies_controller.rb +3 -3
- data/app/controllers/protege/responsibilities_controller.rb +7 -7
- data/app/controllers/protege/threads_controller.rb +4 -4
- data/app/controllers/protege/toolkits_controller.rb +84 -0
- data/app/helpers/protege/agents_helper.rb +75 -0
- data/app/helpers/protege/application_helper.rb +3 -2
- data/app/helpers/protege/components/badges_helper.rb +11 -2
- data/app/helpers/protege/components/forms_helper.rb +47 -12
- data/app/helpers/protege/components/typography_helper.rb +1 -1
- data/app/helpers/protege/messages_helper.rb +2 -2
- data/app/helpers/protege/responsibilities_helper.rb +2 -2
- data/app/helpers/protege/threads_helper.rb +11 -11
- data/app/helpers/protege/toolkits_helper.rb +66 -0
- data/app/hooks/protege/event_logger_hook.rb +8 -8
- data/app/jobs/protege/console_inference_job.rb +8 -8
- data/app/jobs/protege/inference_job.rb +11 -11
- data/app/jobs/protege/responsibility_job.rb +10 -10
- data/app/mailboxes/protege/agent_mailbox.rb +54 -29
- data/app/mailers/protege/alert_mailer.rb +41 -0
- data/app/mailers/protege/application_mailer.rb +9 -8
- data/app/models/concerns/protege/broadcastable_agent.rb +50 -0
- data/app/models/concerns/protege/broadcastable_toolkit.rb +49 -0
- data/app/models/concerns/protege/toolable.rb +108 -0
- data/app/models/protege/access_rule.rb +12 -12
- data/app/models/protege/{persona.rb → agent.rb} +45 -45
- data/app/models/protege/agent_toolkit.rb +50 -0
- data/app/models/protege/agent_toolkit_rule.rb +25 -0
- data/app/models/protege/application_record.rb +1 -1
- data/app/models/protege/email_thread.rb +16 -13
- data/app/models/protege/message.rb +36 -20
- data/app/models/protege/responsibility.rb +12 -12
- data/app/models/protege/tool_use.rb +6 -6
- data/app/models/protege/toolkit.rb +115 -0
- data/app/models/protege/trace.rb +1 -1
- data/app/providers/protege/open_router_provider.rb +17 -0
- data/app/resolvers/README.md +4 -4
- data/app/resolvers/protege/load_file_resolver.rb +5 -5
- data/app/resolvers/protege/load_text_resolver.rb +3 -3
- data/app/resolvers/protege/thread_history_resolver.rb +1 -1
- data/app/services/protege/message_search.rb +2 -2
- data/app/services/protege/system_toolkits.rb +67 -0
- data/app/tools/protege/create_file_tool.rb +2 -0
- data/app/tools/protege/read_attachment_tool.rb +3 -1
- data/app/tools/protege/search_emails_tool.rb +15 -13
- data/app/tools/protege/send_email_tool.rb +60 -18
- data/app/tools/protege/web_fetch_tool.rb +2 -0
- data/app/tools/protege/web_search_tool.rb +3 -1
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/protege/access_rules/create.turbo_stream.slim +3 -3
- data/app/views/protege/agent_toolkit_rules/create.turbo_stream.slim +6 -0
- data/app/views/protege/agent_toolkits/_gate_rule.html.slim +7 -0
- data/app/views/protege/agent_toolkits/_gate_rule_form.html.slim +8 -0
- data/app/views/protege/agent_toolkits/_gate_rules.html.slim +19 -0
- data/app/views/protege/agent_toolkits/show.html.slim +17 -0
- data/app/views/protege/{personas → agents}/_access_rule.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rule_form.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rules.html.slim +5 -5
- data/app/views/protege/agents/_actions.html.slim +15 -0
- data/app/views/protege/agents/_agent_sidebar_item.html.slim +5 -0
- data/app/views/protege/agents/_agent_toolkit.html.slim +7 -0
- data/app/views/protege/agents/_agent_toolkit_form.html.slim +7 -0
- data/app/views/protege/agents/_form.html.slim +25 -0
- data/app/views/protege/agents/_sidebar.html.slim +11 -0
- data/app/views/protege/agents/_toolkits.html.slim +19 -0
- data/app/views/protege/agents/agent_toolkits/create.turbo_stream.slim +6 -0
- data/app/views/protege/agents/edit.html.slim +7 -0
- data/app/views/protege/agents/index.html.slim +6 -0
- data/app/views/protege/agents/new.html.slim +7 -0
- data/app/views/protege/agents/show.html.slim +29 -0
- data/app/views/protege/alert_mailer/inference_failed.text.erb +4 -0
- data/app/views/protege/email_domains/show.html.slim +1 -1
- data/app/views/protege/home/show.html.slim +13 -13
- data/app/views/protege/responsibilities/_form.html.slim +2 -2
- data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +1 -1
- data/app/views/protege/responsibilities/new.html.slim +1 -1
- data/app/views/protege/responsibilities/show.html.slim +2 -2
- data/app/views/protege/responsibility_runs/show.html.slim +1 -1
- data/app/views/protege/shared/_header.html.slim +2 -1
- data/app/views/protege/shared/_hotkeys.html.slim +15 -11
- data/app/views/protege/threads/new.html.slim +2 -2
- data/app/views/protege/toolkits/_form.html.slim +13 -0
- data/app/views/protege/toolkits/_sidebar.html.slim +11 -0
- data/app/views/protege/toolkits/_toolkit_sidebar_item.html.slim +5 -0
- data/app/views/protege/toolkits/edit.html.slim +7 -0
- data/app/views/protege/toolkits/index.html.slim +6 -0
- data/app/views/protege/toolkits/new.html.slim +7 -0
- data/app/views/protege/toolkits/show.html.slim +13 -0
- data/config/routes.rb +12 -1
- data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +1 -1
- data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +1 -1
- data/db/migrate/20260728140000_scope_message_and_thread_uniqueness_per_persona.rb +16 -0
- data/db/migrate/20260812000001_create_protege_toolkits.rb +37 -0
- data/db/migrate/20260815000001_remove_disabled_tool_ids_from_personas.rb +21 -0
- data/db/migrate/20260815000002_add_key_to_protege_toolkits.rb +22 -0
- data/db/migrate/20260815100000_rename_personas_to_agents.rb +24 -0
- data/lib/generators/protege/agent/agent_generator.rb +35 -0
- data/lib/generators/protege/{persona/templates/persona.rb.tt → agent/templates/agent.rb.tt} +9 -9
- data/lib/generators/protege/extension_naming.rb +1 -1
- data/lib/generators/protege/hook/templates/hook.rb.tt +2 -2
- data/lib/generators/protege/install/install_generator.rb +16 -11
- data/lib/generators/protege/install/templates/initializer.rb.tt +17 -6
- data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +2 -2
- data/lib/generators/protege/resolver/resolver_generator.rb +2 -2
- data/lib/generators/protege/resolver/templates/resolver.rb.tt +4 -4
- data/lib/generators/protege/tool/templates/tool.rb.tt +7 -5
- data/lib/protege/configuration.rb +43 -8
- data/lib/protege/engine.rb +3 -1
- data/lib/protege/errors/tool_not_available_error.rb +7 -6
- data/lib/protege/events/event.rb +2 -2
- data/lib/protege/events/inference_chunk_event.rb +1 -1
- data/lib/protege/events/inference_completed_event.rb +1 -1
- data/lib/protege/events/inference_failed_event.rb +1 -1
- data/lib/protege/events/inference_generated_event.rb +1 -1
- data/lib/protege/events/inference_max_turns_reached_event.rb +1 -1
- data/lib/protege/events/inference_started_event.rb +1 -1
- data/lib/protege/events/loop_run_completed_event.rb +1 -1
- data/lib/protege/events/loop_run_enqueued_event.rb +1 -1
- data/lib/protege/events/loop_run_failed_event.rb +1 -1
- data/lib/protege/events/loop_run_started_event.rb +1 -1
- data/lib/protege/events/tool_call_completed_event.rb +1 -1
- data/lib/protege/events/tool_call_failed_event.rb +1 -1
- data/lib/protege/events/tool_call_started_event.rb +1 -1
- data/lib/protege/events/tool_calls_received_event.rb +1 -1
- data/lib/protege/extensions/hook.rb +1 -0
- data/lib/protege/extensions/hook_mixin.rb +1 -1
- data/lib/protege/extensions/manifest.rb +67 -0
- data/lib/protege/extensions/manifested.rb +81 -0
- data/lib/protege/extensions/provider.rb +1 -0
- data/lib/protege/extensions/provider_mixin.rb +9 -17
- data/lib/protege/extensions/resolver.rb +2 -1
- data/lib/protege/extensions/resolver_mixin.rb +2 -2
- data/lib/protege/extensions/tool.rb +1 -0
- data/lib/protege/extensions/tool_mixin.rb +16 -40
- data/lib/protege/gateway/access_control.rb +19 -19
- data/lib/protege/gateway/access_policy.rb +2 -2
- data/lib/protege/gateway/mail/outbound.rb +22 -5
- data/lib/protege/gateway.rb +48 -37
- data/lib/protege/loop/scheduler.rb +1 -1
- data/lib/protege/orchestrator/context.rb +8 -8
- data/lib/protege/orchestrator/harness.rb +26 -24
- data/lib/protege/orchestrator/reply_context.rb +3 -3
- data/lib/protege/orchestrator/reply_harness.rb +8 -8
- data/lib/protege/orchestrator/resolver_chain.rb +3 -3
- data/lib/protege/orchestrator/responsibility_context.rb +4 -4
- data/lib/protege/orchestrator/responsibility_harness.rb +8 -8
- data/lib/protege/orchestrator.rb +7 -7
- data/lib/protege/subscribers/alerter.rb +117 -0
- data/lib/protege/subscribers/tracing.rb +1 -1
- data/lib/protege/version.rb +1 -1
- data/lib/protege.rb +2 -2
- data/lib/tasks/protege_tasks.rake +12 -0
- metadata +66 -23
- data/app/controllers/concerns/protege/persona_scoped.rb +0 -22
- data/app/controllers/protege/personas_controller.rb +0 -118
- data/app/helpers/protege/personas_helper.rb +0 -90
- data/app/models/concerns/protege/broadcastable_persona.rb +0 -50
- data/app/models/concerns/protege/tool_scoped.rb +0 -88
- data/app/views/protege/personas/_actions.html.slim +0 -15
- data/app/views/protege/personas/_form.html.slim +0 -29
- data/app/views/protege/personas/_persona_sidebar_item.html.slim +0 -5
- data/app/views/protege/personas/_sidebar.html.slim +0 -11
- data/app/views/protege/personas/edit.html.slim +0 -7
- data/app/views/protege/personas/index.html.slim +0 -6
- data/app/views/protege/personas/new.html.slim +0 -7
- data/app/views/protege/personas/show.html.slim +0 -37
- data/lib/generators/protege/persona/persona_generator.rb +0 -35
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
= sidebar_item(responsibility.name, responsibility_path(responsibility),
|
|
2
2
|
detail: responsibility_status(responsibility),
|
|
3
|
-
subtitle: "#{responsibility.
|
|
3
|
+
subtitle: "#{responsibility.agent.name} · #{responsibility.schedule}",
|
|
4
4
|
active: local_assigns[:active],
|
|
5
5
|
id: dom_id(responsibility))
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
= render "sidebar"
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header("New responsibility", "A scheduled duty
|
|
5
|
+
= page_header("New responsibility", "A scheduled duty an agent carries out on a cron schedule.")
|
|
6
6
|
|
|
7
7
|
= card { render "form", responsibility: @responsibility }
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
= render "sidebar", current: @responsibility
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header(@responsibility.name, "#{@responsibility.
|
|
5
|
+
= page_header(@responsibility.name, "#{@responsibility.agent.name} · #{@responsibility.schedule}") do
|
|
6
6
|
div
|
|
7
7
|
= ui_link("Edit", edit_responsibility_path(@responsibility), class: 'px-3')
|
|
8
8
|
= inline_button("Run now", responsibility_runs_path, params: { responsibility_run: { responsibility_id: @responsibility.id } })
|
|
9
9
|
|
|
10
10
|
.space-y-4
|
|
11
|
-
= info_card("Schedule", { "Cron" => @responsibility.schedule, "
|
|
11
|
+
= info_card("Schedule", { "Cron" => @responsibility.schedule, "Agent" => @responsibility.agent.name, "Active" => (@responsibility.active? ? "Yes" : "No") })
|
|
12
12
|
|
|
13
13
|
= card do
|
|
14
14
|
= heading_2("Instructions")
|
|
@@ -4,8 +4,9 @@ header class="flex h-14 flex-shrink-0 items-center justify-between px-5" style="
|
|
|
4
4
|
|
|
5
5
|
nav class="flex items-center gap-1"
|
|
6
6
|
= nav_item("Inbox", protege.threads_path, request.path)
|
|
7
|
+
= nav_item("Agents", protege.agents_path, request.path)
|
|
8
|
+
= nav_item("Toolkits", protege.toolkits_path, request.path)
|
|
7
9
|
= nav_item("Responsibilities", protege.responsibilities_path, request.path)
|
|
8
|
-
= nav_item("Personas", protege.personas_path, request.path)
|
|
9
10
|
= nav_item("Domains", protege.email_domains_path, request.path)
|
|
10
11
|
= nav_item("Traces", protege.traces_path, request.path)
|
|
11
12
|
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
/
|
|
4
4
|
/ Registry — the single source of truth for app-wide combos. Each is a hidden link keys.js activates on
|
|
5
5
|
/ its combo; it navigates via the same route helper the visible UI uses, so a global command works from
|
|
6
|
-
/ anywhere, independent of what's on screen. Nav combos are numbered by position (1..
|
|
6
|
+
/ anywhere, independent of what's on screen. Nav combos are numbered by position (1..6, left to right)
|
|
7
7
|
/ rather than by initial — the "go to the Nth section" convention (⌘1..9 in browsers/editors). Rendered
|
|
8
8
|
/ after the page body, so a page-local hotkey reusing a combo is matched first and shadows the global one.
|
|
9
9
|
div hidden=true aria-hidden="true"
|
|
10
10
|
= link_to "Documentation", 'https://docs.protege.bot', data: { hotkey: "g 0" }, target: '_blank'
|
|
11
11
|
= link_to "Inbox", protege.threads_path, data: { hotkey: "g 1" }
|
|
12
|
-
= link_to "
|
|
13
|
-
= link_to "
|
|
14
|
-
= link_to "
|
|
15
|
-
= link_to "
|
|
12
|
+
= link_to "Agents", protege.agents_path, data: { hotkey: "g 2" }
|
|
13
|
+
= link_to "Toolkits", protege.toolkits_path, data: { hotkey: "g 3" }
|
|
14
|
+
= link_to "Responsibilities", protege.responsibilities_path, data: { hotkey: "g 4" }
|
|
15
|
+
= link_to "Domains", protege.email_domains_path, data: { hotkey: "g 5" }
|
|
16
|
+
= link_to "Traces", protege.traces_path, data: { hotkey: "g 6" }
|
|
16
17
|
= link_to "New Thread", protege.new_thread_path, data: { hotkey: "g c" }
|
|
18
|
+
= link_to "New Agent", protege.new_agent_path, data: { hotkey: "g a" }
|
|
19
|
+
= link_to "New Toolkit", protege.new_toolkit_path, data: { hotkey: "g k" }
|
|
17
20
|
= link_to "New Responsibility", protege.new_responsibility_path, data: { hotkey: "g r" }
|
|
18
|
-
= link_to "New Persona", protege.new_persona_path, data: { hotkey: "g p" }
|
|
19
21
|
= link_to "New Email Domain", protege.new_email_domain_path, data: { hotkey: "g d" }
|
|
20
22
|
|
|
21
23
|
/ Help overlay, opened with "?" or the header's "?" button (keys.js just toggles it open/closed). This
|
|
@@ -26,16 +28,18 @@ div hidden=true aria-hidden="true"
|
|
|
26
28
|
.mt-3.space-y-1
|
|
27
29
|
= hotkey_section("Go to")
|
|
28
30
|
= hotkey_row("Inbox", "g", "1")
|
|
29
|
-
= hotkey_row("
|
|
30
|
-
= hotkey_row("
|
|
31
|
-
= hotkey_row("
|
|
32
|
-
= hotkey_row("
|
|
31
|
+
= hotkey_row("Agents", "g", "2")
|
|
32
|
+
= hotkey_row("Toolkits", "g", "3")
|
|
33
|
+
= hotkey_row("Responsibilities", "g", "4")
|
|
34
|
+
= hotkey_row("Domains", "g", "5")
|
|
35
|
+
= hotkey_row("Traces", "g", "6")
|
|
33
36
|
= hotkey_row("Documentation", "g", "0")
|
|
34
37
|
|
|
35
38
|
= hotkey_section("Create")
|
|
36
39
|
= hotkey_row("New thread", "g", "c")
|
|
40
|
+
= hotkey_row("New agent", "g", "a")
|
|
41
|
+
= hotkey_row("New toolkit", "g", "k")
|
|
37
42
|
= hotkey_row("New responsibility", "g", "r")
|
|
38
|
-
= hotkey_row("New persona", "g", "p")
|
|
39
43
|
= hotkey_row("New email domain", "g", "d")
|
|
40
44
|
|
|
41
45
|
= hotkey_section("General")
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
= render "protege/threads/sidebar", threads: inbox_threads
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header("New message", "Send a message to
|
|
5
|
+
= page_header("New message", "Send a message to an agent to start a thread.")
|
|
6
6
|
|
|
7
7
|
= card do
|
|
8
8
|
= form_with url: threads_path, scope: :thread, class: "space-y-5", multipart: true do |f|
|
|
9
|
-
= form_select(f, :
|
|
9
|
+
= form_select(f, :agent_id, available_agent_options, label: "To", prompt: "Select agent...")
|
|
10
10
|
|
|
11
11
|
= form_field(f, :subject, label: "Subject", placeholder: "Subject")
|
|
12
12
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
= form_with model: toolkit, class: "space-y-5" do |f|
|
|
2
|
+
= form_errors(toolkit)
|
|
3
|
+
|
|
4
|
+
= form_field(f, :name, placeholder: "e.g. Finance")
|
|
5
|
+
|
|
6
|
+
= form_text_area(f, :description, label: "Description", rows: 3, hint: "What this toolkit is for — shown only in the dashboard.")
|
|
7
|
+
|
|
8
|
+
- tool_id = ->(entry) { entry.id.to_s }
|
|
9
|
+
= form_checklist(f, :member_tool_ids, toolkit_tool_options, label: "Tools", value: tool_id, text: :display_name, description: :summary, hint: "The tools this toolkit bundles. Who may use them is set per agent when you attach it.")
|
|
10
|
+
|
|
11
|
+
.flex.items-center.gap-3.pt-2
|
|
12
|
+
= button("Save toolkit", type: :submit)
|
|
13
|
+
= ui_link("Cancel", toolkits_path)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
- pagy, toolkits = paginate(all_toolkits)
|
|
2
|
+
|
|
3
|
+
= sidebar_content do
|
|
4
|
+
= sidebar_header("Toolkits (#{pagy.count})", new_path: new_toolkit_path)
|
|
5
|
+
|
|
6
|
+
= sidebar_stream("protege:toolkits", pagy: pagy)
|
|
7
|
+
|
|
8
|
+
= sidebar_body(toolkits, empty: "No toolkits yet", id: "toolkits_list") do |toolkit|
|
|
9
|
+
= render "toolkit_sidebar_item", toolkit: toolkit, active: local_assigns[:current] == toolkit
|
|
10
|
+
|
|
11
|
+
= sidebar_pagination(pagy)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
- content_for :sidebar do
|
|
2
|
+
= render "sidebar", current: @toolkit
|
|
3
|
+
|
|
4
|
+
= page_container do
|
|
5
|
+
= page_header(@toolkit.name, @toolkit.description) do
|
|
6
|
+
div
|
|
7
|
+
= ui_link("Edit", edit_toolkit_path(@toolkit), class: "px-3")
|
|
8
|
+
- unless @toolkit.system?
|
|
9
|
+
= destructive_link("Delete", toolkit_path(@toolkit), confirm: "Delete this toolkit?", method: :delete)
|
|
10
|
+
|
|
11
|
+
.space-y-4
|
|
12
|
+
= info_card("Toolkit", toolkit_overview(@toolkit))
|
|
13
|
+
= hint("This toolkit is a reusable set of tools. Who may use them is decided per agent — attach it on an agent's page to set its sender gate.")
|
data/config/routes.rb
CHANGED
|
@@ -18,13 +18,24 @@ Protege::Engine.routes.draw do
|
|
|
18
18
|
resources :responsibilities
|
|
19
19
|
resources :responsibility_runs
|
|
20
20
|
|
|
21
|
+
resources :toolkits
|
|
22
|
+
|
|
23
|
+
# A grant (agent↔toolkit) is split across two controllers so nesting never goes two deep. The
|
|
24
|
+
# *nested* Agents::AgentToolkitsController lists an agent's grants and attaches/detaches them (in
|
|
25
|
+
# the agent's scope); the *top-level* AgentToolkitsController manages a created grant on its own
|
|
26
|
+
# page, where its sender-gate rules nest one level deep (like access_rules under an agent).
|
|
27
|
+
resources :agent_toolkits do
|
|
28
|
+
resources :agent_toolkit_rules
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
resources :threads do
|
|
22
32
|
resource :reply
|
|
23
33
|
end
|
|
24
34
|
|
|
25
|
-
resources :
|
|
35
|
+
resources :agents do
|
|
26
36
|
resource :archive
|
|
27
37
|
resources :access_rules
|
|
38
|
+
resources :agent_toolkits, module: :agents
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
resources :email_domains do
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Replace the persona +active+ boolean with an +archived_at+ timestamp — the idiomatic soft-delete shape,
|
|
4
4
|
# which also records *when* a persona was archived. The model keeps +active?+/+scope :active+ as derived
|
|
5
5
|
# readers over +archived_at+, so routing/scheduler/status code is unchanged.
|
|
6
|
-
class ReplacePersonaActiveWithArchivedAt < ActiveRecord::Migration[
|
|
6
|
+
class ReplacePersonaActiveWithArchivedAt < ActiveRecord::Migration[7.2]
|
|
7
7
|
# Add archived_at, mark already-deactivated personas as archived-now, then drop active.
|
|
8
8
|
#
|
|
9
9
|
# @return [void]
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# has switched *off* within that grant, so scoping narrows and never widens. Stored as a JSON array in a
|
|
6
6
|
# text column (SQLite has no native JSON type), matching +Message+'s recipient-list convention, and
|
|
7
7
|
# defaults to an empty array so an unconfigured persona keeps its full grant.
|
|
8
|
-
class AddDisabledToolIdsToPersonas < ActiveRecord::Migration[
|
|
8
|
+
class AddDisabledToolIdsToPersonas < ActiveRecord::Migration[7.2]
|
|
9
9
|
# Add the JSON-array text column, non-null with an empty-array default.
|
|
10
10
|
#
|
|
11
11
|
# @return [void]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Scopes inbound-mail uniqueness per persona, so one email addressed to several proteges can be handled
|
|
4
|
+
# by each of them independently. Previously +thread_id+ and +message_id+ were globally unique, which
|
|
5
|
+
# assumed exactly one persona per inbound message: the second persona's thread/message insert collided on
|
|
6
|
+
# the global index. Making both unique per +persona_id+ lets every addressed persona own its own thread
|
|
7
|
+
# and message for the same RFC Message-ID, while still keeping a single persona's retries idempotent.
|
|
8
|
+
class ScopeMessageAndThreadUniquenessPerPersona < ActiveRecord::Migration[7.2]
|
|
9
|
+
def change
|
|
10
|
+
remove_index :protege_email_threads, column: :thread_id, unique: true
|
|
11
|
+
add_index :protege_email_threads, %i[persona_id thread_id], unique: true
|
|
12
|
+
|
|
13
|
+
remove_index :protege_messages, column: :message_id, unique: true
|
|
14
|
+
add_index :protege_messages, %i[persona_id message_id], unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Creates the toolkit binding tables — the first "Binding" of the extension framework. A toolkit is a
|
|
4
|
+
# global, dashboard-managed, reusable bundle of tools (referenced by their manifest ids, stored as a JSON
|
|
5
|
+
# id list like +disabled_tool_ids+) — nothing more. Permission lives on the *attachment*: a
|
|
6
|
+
# +protege_persona_toolkits+ row gives a toolkit to one persona and carries that grant's sender gate
|
|
7
|
+
# (child +persona_toolkit_rules+ rows, kind + pattern, mirroring +AccessRule+) plus +allow_proactive+ (may
|
|
8
|
+
# these tools run on that persona's scheduled, senderless runs?). So the same toolkit attached to two
|
|
9
|
+
# personas can be gated differently, with no tool-list duplication.
|
|
10
|
+
class CreateProtegeToolkits < ActiveRecord::Migration[7.2]
|
|
11
|
+
def change
|
|
12
|
+
create_table :protege_toolkits do |t|
|
|
13
|
+
t.string :name, null: false, index: { unique: true }
|
|
14
|
+
t.text :description
|
|
15
|
+
t.text :member_tool_ids, null: false, default: '[]'
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
create_table :protege_persona_toolkits do |t|
|
|
21
|
+
t.references :persona, null: false, foreign_key: { to_table: :protege_personas }
|
|
22
|
+
t.references :toolkit, null: false, foreign_key: { to_table: :protege_toolkits }
|
|
23
|
+
t.boolean :allow_proactive, null: false, default: false
|
|
24
|
+
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
add_index :protege_persona_toolkits, %i[persona_id toolkit_id], unique: true
|
|
28
|
+
|
|
29
|
+
create_table :protege_persona_toolkit_rules do |t|
|
|
30
|
+
t.references :persona_toolkit, null: false, foreign_key: { to_table: :protege_persona_toolkits }
|
|
31
|
+
t.string :kind, null: false
|
|
32
|
+
t.string :pattern, null: false
|
|
33
|
+
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Remove +disabled_tool_ids+ from personas. Tool access no longer flows through a per-persona ceiling (a
|
|
4
|
+
# code-declared +tools+ grant minus this disable list); a persona's tools now come entirely from the
|
|
5
|
+
# toolkits attached to it, gated per attachment (see +Protege::Toolable+ / +PersonaToolkit+). The
|
|
6
|
+
# subtractive override this column backed no longer exists, so the column is dropped.
|
|
7
|
+
class RemoveDisabledToolIdsFromPersonas < ActiveRecord::Migration[7.2]
|
|
8
|
+
# Drop the column.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def up
|
|
12
|
+
remove_column :protege_personas, :disabled_tool_ids
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Restore the JSON-array text column, non-null with an empty-array default.
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
def down
|
|
19
|
+
add_column :protege_personas, :disabled_tool_ids, :text, null: false, default: '[]'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add a stable +key+ to toolkits so the engine can identify its *system* toolkits — the auto-maintained
|
|
4
|
+
# "All Tools" (+all_tools+) and the auto-attached "Default Tools" (+default_tools+) — independently of
|
|
5
|
+
# their display name (which operators may rename). User-created toolkits leave +key+ null; a unique index
|
|
6
|
+
# (SQLite allows multiple NULLs) keeps at most one toolkit per system key.
|
|
7
|
+
#
|
|
8
|
+
# Installs seeded before system toolkits existed already have an "All Tools" toolkit (created by the old
|
|
9
|
+
# host seed). Adopt it as the keyed system toolkit here, so the sync updates that record in place rather
|
|
10
|
+
# than colliding on its unique name.
|
|
11
|
+
class AddKeyToProtegeToolkits < ActiveRecord::Migration[7.2]
|
|
12
|
+
def up
|
|
13
|
+
add_column :protege_toolkits, :key, :string
|
|
14
|
+
add_index :protege_toolkits, :key, unique: true
|
|
15
|
+
|
|
16
|
+
execute("UPDATE protege_toolkits SET key = 'all_tools' WHERE name = 'All Tools' AND key IS NULL")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def down
|
|
20
|
+
remove_column :protege_toolkits, :key
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rename the Persona concept to Agent across the schema — the industry has settled on "agent" for the
|
|
4
|
+
# addressable actor, and the code now follows suit (+Protege::Agent+, +AgentToolkit+, …). Pure renames,
|
|
5
|
+
# so existing installs transition in place with their data intact: the three persona tables become agent
|
|
6
|
+
# tables, and every +persona_id+ foreign key becomes +agent_id+. Rails renames the conventionally-named
|
|
7
|
+
# indexes along with each table/column, and SQLite keeps foreign-key references pointed at the renamed
|
|
8
|
+
# tables. Earlier migrations keep their historical persona names — on a fresh install they run first and
|
|
9
|
+
# this migration renames their output.
|
|
10
|
+
class RenamePersonasToAgents < ActiveRecord::Migration[7.2]
|
|
11
|
+
def change
|
|
12
|
+
rename_table :protege_personas, :protege_agents
|
|
13
|
+
rename_table :protege_persona_toolkits, :protege_agent_toolkits
|
|
14
|
+
rename_table :protege_persona_toolkit_rules, :protege_agent_toolkit_rules
|
|
15
|
+
|
|
16
|
+
rename_column :protege_email_threads, :persona_id, :agent_id
|
|
17
|
+
rename_column :protege_messages, :persona_id, :agent_id
|
|
18
|
+
rename_column :protege_access_rules, :persona_id, :agent_id
|
|
19
|
+
rename_column :protege_responsibilities, :persona_id, :agent_id
|
|
20
|
+
rename_column :protege_tool_uses, :persona_id, :agent_id
|
|
21
|
+
rename_column :protege_agent_toolkits, :persona_id, :agent_id
|
|
22
|
+
rename_column :protege_agent_toolkit_rules, :persona_toolkit_id, :agent_toolkit_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/named_base'
|
|
4
|
+
require_relative '../extension_naming'
|
|
5
|
+
|
|
6
|
+
module Protege
|
|
7
|
+
module Generators
|
|
8
|
+
# Scaffolds an agent — the identity Protege binds an inbox to — into the host app's agents
|
|
9
|
+
# directory (+config.agents_path+, default +app/agents+). An agent is an STI subclass of
|
|
10
|
+
# +Protege::Agent+ (an ActiveRecord model); subclassing registers the subtype, and each agent
|
|
11
|
+
# declares its display name and the resolver chains that assemble context for its reply and
|
|
12
|
+
# scheduled runs.
|
|
13
|
+
#
|
|
14
|
+
# The +Agent+ suffix is applied for you, so all of +support+, +Support+, and +SupportAgent+
|
|
15
|
+
# produce class +SupportAgent+ in +support_agent.rb+.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# bin/rails g protege:agent support
|
|
19
|
+
class AgentGenerator < Rails::Generators::NamedBase
|
|
20
|
+
include ExtensionNaming
|
|
21
|
+
|
|
22
|
+
source_root File.expand_path('templates', __dir__)
|
|
23
|
+
|
|
24
|
+
desc 'Scaffold a Protege::Agent subclass in the configured agents path (default app/agents).'
|
|
25
|
+
|
|
26
|
+
# Render the agent scaffold into +config.agents_path+.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def create_agent
|
|
30
|
+
template 'agent.rb.tt',
|
|
31
|
+
File.join(Protege.configuration.agents_path, "#{extension_file_name('Agent')}.rb")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# <%= extension_class_name('
|
|
3
|
+
# <%= extension_class_name('Agent') %> — an agent: the identity Protege binds an inbox to.
|
|
4
4
|
#
|
|
5
|
-
# Subclassing +Protege::
|
|
5
|
+
# Subclassing +Protege::Agent+ (an STI ActiveRecord model) registers this subtype. Create the row in
|
|
6
6
|
# the dashboard (or a seed) to give it a +name+, +email_address+, and +instructions+ (its editable
|
|
7
7
|
# system prompt); inbound mail to that address routes here, and outbound replies are sent as it.
|
|
8
8
|
#
|
|
9
9
|
# The two chains below assemble the context sent to the model. Engine built-in resolvers are named by
|
|
10
10
|
# their flat +Protege::+ constant; host-app resolvers (see <%= Protege.configuration.resolvers_path %>/) by their own top-level constant.
|
|
11
|
-
class <%= extension_class_name('
|
|
12
|
-
# Human-friendly label shown for this
|
|
13
|
-
self.display_name = '<%= extension_class_name('
|
|
11
|
+
class <%= extension_class_name('Agent') %> < Protege::Agent
|
|
12
|
+
# Human-friendly label shown for this agent in the dashboard, logs, and tooling.
|
|
13
|
+
self.display_name = '<%= extension_class_name('Agent').titleize %>'
|
|
14
14
|
|
|
15
15
|
# Context assembled before every reply to inbound mail. +Protege::LoadTextResolver+ supplies this
|
|
16
|
-
#
|
|
16
|
+
# agent's dashboard-editable +instructions+ as the system prompt; +Protege::ThreadHistoryResolver+
|
|
17
17
|
# replays the conversation so far. Insert your own resolvers between them (e.g. to inject records,
|
|
18
18
|
# summaries, or retrieved facts).
|
|
19
19
|
message_resolvers do |chain|
|
|
20
|
-
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.
|
|
20
|
+
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
|
|
21
21
|
chain.use Protege::ThreadHistoryResolver
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Context assembled for a scheduled run (the Loop layer). There is no inbound thread, so the chain is
|
|
25
|
-
# just identity: the
|
|
25
|
+
# just identity: the agent's +instructions+ as the system prompt and the responsibility's own
|
|
26
26
|
# prompt as the opening user message. Add task-specific resolvers here if a responsibility needs more.
|
|
27
27
|
responsibility_resolvers do |chain|
|
|
28
|
-
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.
|
|
28
|
+
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
|
|
29
29
|
chain.use(Protege::LoadTextResolver, role: :user) { |ctx| ctx.responsibility.instructions }
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -9,7 +9,7 @@ module Protege
|
|
|
9
9
|
#
|
|
10
10
|
# The suffix is applied whether or not it was supplied, so all three of +send_email+, +SendEmail+,
|
|
11
11
|
# and +SendEmailTool+ resolve to class +SendEmailTool+ / file +send_email_tool.rb+. Pass an empty
|
|
12
|
-
# suffix (as the
|
|
12
|
+
# suffix (as the agent generator does) to use the name verbatim.
|
|
13
13
|
module ExtensionNaming
|
|
14
14
|
private
|
|
15
15
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Subclassing +Protege::Hook+ wires these handlers onto the event bus at boot (auto-discovered, no
|
|
6
6
|
# registration). Each +on+ block receives a typed, frozen +Protege::Event+ and runs isolated — a raised
|
|
7
7
|
# error is swallowed and logged, never breaking the run. Read event fields with nil-safe named readers
|
|
8
|
-
# (+event.
|
|
8
|
+
# (+event.agent+, +event.tool_call+, +event.result+, +event.error+, +event.correlation_id+).
|
|
9
9
|
#
|
|
10
10
|
# Declare a handler with +on(EventClass) { |event| ... }+ (pass several classes to share one block).
|
|
11
11
|
# Available events (all +Protege::+ constants), in fire order:
|
|
@@ -21,7 +21,7 @@ class <%= extension_class_name('Hook') %> < Protege::Hook
|
|
|
21
21
|
|
|
22
22
|
# TODO: replace with the events you care about.
|
|
23
23
|
on InferenceStartedEvent do |event|
|
|
24
|
-
log "inference started for
|
|
24
|
+
log "inference started for agent=#{event.agent&.name} (#{event.correlation_id})"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rails/generators/base'
|
|
4
|
-
require_relative '../
|
|
4
|
+
require_relative '../agent/agent_generator'
|
|
5
5
|
|
|
6
6
|
module Protege
|
|
7
7
|
module Generators
|
|
8
8
|
# Bootstraps Protege into a host Rails app — the one command to go from +gem 'protege'+ to a
|
|
9
9
|
# runnable dashboard. It installs the Rails frameworks Protege depends on (Active Storage for
|
|
10
10
|
# attachments, Action Mailbox for inbound mail), writes a fully-documented initializer, mounts the
|
|
11
|
-
# engine, wires the Loop's recurring tick, and scaffolds a starter
|
|
11
|
+
# engine, wires the Loop's recurring tick, and scaffolds a starter agent. The steps it cannot do
|
|
12
12
|
# safely for you — wrapping the mount in your own authentication, running migrations, setting
|
|
13
13
|
# credentials — are printed as next steps.
|
|
14
14
|
#
|
|
@@ -17,7 +17,7 @@ module Protege
|
|
|
17
17
|
class InstallGenerator < Rails::Generators::Base
|
|
18
18
|
source_root File.expand_path('templates', __dir__)
|
|
19
19
|
|
|
20
|
-
desc 'Install Protege into the host app: initializer, engine mount, recurring tick, and a starter
|
|
20
|
+
desc 'Install Protege into the host app: initializer, engine mount, recurring tick, and a starter agent.'
|
|
21
21
|
|
|
22
22
|
# The recurring job entry that drives the Loop scheduler — one tick a minute fires due
|
|
23
23
|
# responsibilities. Written at column zero; {#indented_tick} nests it under an environment key.
|
|
@@ -76,12 +76,12 @@ module Protege
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
# Scaffold a starter +
|
|
80
|
-
# truth for the
|
|
79
|
+
# Scaffold a starter +ExecutiveAgent+ by delegating to the agent generator (single source of
|
|
80
|
+
# truth for the agent template).
|
|
81
81
|
#
|
|
82
82
|
# @return [void]
|
|
83
|
-
def
|
|
84
|
-
invoke Protege::Generators::
|
|
83
|
+
def create_starter_agent
|
|
84
|
+
invoke Protege::Generators::AgentGenerator, ['Executive']
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Print the wiring the generator can't safely automate.
|
|
@@ -96,18 +96,23 @@ module Protege
|
|
|
96
96
|
1. Run the migrations (Active Storage, Action Mailbox, and Protege's own):
|
|
97
97
|
bin/rails db:migrate
|
|
98
98
|
|
|
99
|
-
2.
|
|
99
|
+
2. Sync the system toolkits (creates "All Tools" and "Default Tools"; agents you create
|
|
100
|
+
afterwards auto-attach Default Tools, so they can reply out of the box). Run it on
|
|
101
|
+
every deploy, after db:migrate:
|
|
102
|
+
bin/rails protege:toolkits:sync
|
|
103
|
+
|
|
104
|
+
3. WRAP THE ENGINE MOUNT in your authentication in config/routes.rb — the engine ships no
|
|
100
105
|
auth of its own. An unwrapped mount exposes the dashboard to anyone.
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
4. Set your provider credentials (the defaults read OpenRouter from ENV):
|
|
103
108
|
OPENROUTER_API_KEY=sk-... (and OPENROUTER_MODEL to pick a model)
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
5. Let extensions register in development by loading them eagerly — add to
|
|
106
111
|
config/environments/development.rb:
|
|
107
112
|
config.eager_load = true
|
|
108
113
|
(tools/providers/hooks are discovered as loaded subclasses).
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
6. Create your first agent + email domain (dashboard, or a db/seeds.rb bootstrap) so
|
|
111
116
|
inbound mail has somewhere to route. Then boot and open /protege.
|
|
112
117
|
STEPS
|
|
113
118
|
end
|
|
@@ -16,8 +16,13 @@ Protege.configure do |config|
|
|
|
16
16
|
|
|
17
17
|
# ── Inference ─────────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
-
# Maximum tool-calling rounds before the harness returns the last response. (default:
|
|
20
|
-
# config.max_tool_turns =
|
|
19
|
+
# Maximum tool-calling rounds before the harness returns the last response. (default: 100)
|
|
20
|
+
# config.max_tool_turns = 100
|
|
21
|
+
|
|
22
|
+
# X-Protege-Recursion hop count at which inbound mail is silently dropped — bounds agent-to-agent
|
|
23
|
+
# reply loops. A human replying anywhere resets the chain (mail clients don't echo the header).
|
|
24
|
+
# (default: 50)
|
|
25
|
+
# config.recursion_limit = 50
|
|
21
26
|
|
|
22
27
|
# Symbolic id of the provider extension to run inference through; must match a registered provider's
|
|
23
28
|
# +protege_id+ (here the built-in OpenRouter's :openrouter). (default: nil)
|
|
@@ -47,8 +52,8 @@ Protege.configure do |config|
|
|
|
47
52
|
|
|
48
53
|
# ── Inbound access control ──────────────────────────────────────────────────
|
|
49
54
|
|
|
50
|
-
# Global ceiling on which senders may reach ANY
|
|
51
|
-
# Per-
|
|
55
|
+
# Global ceiling on which senders may reach ANY agent — the committed layer of the access guardrail.
|
|
56
|
+
# Per-agent rules (managed in the dashboard) only narrow this further, never widen it. Patterns are
|
|
52
57
|
# an exact address or a single "*" wildcard. Defaults to permit-all.
|
|
53
58
|
# config.inbound_access = Protege::Gateway.build_access_policy(allow: ['*@your-company.com'])
|
|
54
59
|
# config.inbound_access = Protege::Gateway.build_access_policy(deny: ['*@spam.example'])
|
|
@@ -68,15 +73,21 @@ Protege.configure do |config|
|
|
|
68
73
|
|
|
69
74
|
# ── Extension scaffold paths ──────────────────────────────────────────────────
|
|
70
75
|
|
|
71
|
-
# Where the `bin/rails g protege:{tool,resolver,hook,
|
|
76
|
+
# Where the `bin/rails g protege:{tool,resolver,hook,agent,provider}` generators write. Defaults to
|
|
72
77
|
# the conventional app/ directories; override if you group extensions elsewhere (must stay under an
|
|
73
78
|
# autoloaded path).
|
|
74
79
|
# config.tools_path = 'app/tools'
|
|
75
80
|
# config.resolvers_path = 'app/resolvers'
|
|
76
81
|
# config.hooks_path = 'app/hooks'
|
|
77
|
-
# config.
|
|
82
|
+
# config.agents_path = 'app/agents'
|
|
78
83
|
# config.providers_path = 'app/providers'
|
|
79
84
|
|
|
85
|
+
# ── Failure alerts ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
# Email the platform operator when an inference run fails (sent via Action Mailer, outside the
|
|
88
|
+
# agent's own mail flow). Off by default; no alert is sent until `to:` is set.
|
|
89
|
+
# config.failure_alerts = { to: ['ops@example.com'], from: 'alerts@your-domain.com' }
|
|
90
|
+
|
|
80
91
|
# ── Logging ─────────────────────────────────────────────────────────────────
|
|
81
92
|
|
|
82
93
|
# Logger for engine output; defaults to Rails.logger. Override to route to a dedicated target:
|
|
@@ -101,7 +101,7 @@ Pre-stage the TXT records anytime. Do **not** change the MX until you cut over (
|
|
|
101
101
|
2. Deploy the app and **boot the accessory** (Kamal `deploy` does not manage accessories):
|
|
102
102
|
`kamal accessory boot mail`.
|
|
103
103
|
3. Point the **MX** at `agent.example.com` (priority 10). Inbound now arrives at your box.
|
|
104
|
-
4. Validate: `kamal accessory logs mail`; email the
|
|
104
|
+
4. Validate: `kamal accessory logs mail`; email the agent from Gmail → reply lands with
|
|
105
105
|
**SPF/DKIM/DMARC = pass** ("Show original"). Score at [mail-tester.com](https://www.mail-tester.com)
|
|
106
106
|
and iterate on reputation.
|
|
107
107
|
|
|
@@ -126,7 +126,7 @@ Our own outbound (the app container, on RFC1918) skips all three — only remote
|
|
|
126
126
|
first-run window: deploy with `monitor`, watch `kamal accessory logs mail` for `dmarc=fail` on
|
|
127
127
|
legitimate senders, then switch to `enforce` and redeploy (no image rebuild needed).
|
|
128
128
|
|
|
129
|
-
To verify: from a Gmail account, email the
|
|
129
|
+
To verify: from a Gmail account, email the agent and confirm a normal message is delivered with
|
|
130
130
|
`dmarc=pass`; a spoofing attempt against a `p=reject` domain is refused at SMTP (visible in the logs).
|
|
131
131
|
|
|
132
132
|
## Rollback
|