.. SPDX-License-Identifier: CC-BY-4.0 SPDX-FileCopyrightText: Netresearch DTT GmbH .. include:: /Includes.rst.txt ========================== ADR-214 historical context ========================== This appendix preserves the decision and its amendments. For present use, follow :ref:`adr-214` and its current guide links. .. _adr-214-context: Context ======== Editors ask for guided processes in the chat: optimise the SEO of one page, improve the content of one page. Such a process picks a page, analyses it, gives an overview, then works through the findings one at a time. Each finding gets a proposal with the current and the proposed text and three answers: apply, another variant, skip. Nothing is saved without an explicit confirmation. The editor sees the progress and the affected content element. A skipped finding stays open for the next conversation. The product owner wants such processes as editable text, not as one extension per process. Skills (:ref:`ADR-035 `, :ref:`ADR-036 `) are the existing record for reusable prose, and the agent run already has the suspension states a guided flow needs. Two product requirements are fixed: a skill can be started by a slash command **and** loaded by the model when its name and description match the request; and skills are enabled per skill, like tools. Seven properties of the current code stand in the way. .. _adr-214-context-framing: 1. Every skill is data, never an instruction -------------------------------------------- :php:`SkillComposer` wraps every admitted skill in the same frame: a guard preamble that calls the block "UNTRUSTED task-reference DATA" and fence markers that say ``Classes/Service/Skill/SkillComposer.php#do not follow as instructions``. :php:`SkillComposer::composeBlock()` applies that frame to every skill :php:`SkillComposer::effectiveSkills()` admits, whatever its :php:`SkillTrustLevel`. The trust level decides admission only: a skill below ``skills.minTrustLevel`` is not composed at all (:ref:`ADR-061 `, item 1; ``ext_conf_template.txt#skills.minTrustLevel = untrusted``; :php:`SkillComposerFactory::minTrustLevel()`). The governance profiles expect ``community``, ``verified`` or ``first_party`` there, and ``development`` expects ``untrusted`` (:php:`GovernanceProfile::expectations()`). A ``first_party`` skill is admitted under every profile and still reaches the model as text it is told not to follow. ADR-036 item 3 puts the block into the first user message and never into the system role; ADR-061 item 4 added the fence. Both were right for third-party text fetched from the internet, and neither leaves room for text the installation itself vouches for. .. _adr-214-context-sources: 2. The checksum covers the body only, and a backend edit invalidates it ----------------------------------------------------------------------- The source types are ``single_file``, ``repo`` and ``marketplace`` (:php:`SkillSourceType`), and every fetch goes through :php:`GitHubClient`, whose host allow-list names only GitHub hosts (``Classes/Service/Skill/GitHubClient.php#ALLOWED_HOSTS``). The sync computes ``body_checksum`` from the body alone (``Classes/Service/Skill/SkillSyncService.php#hash('sha256', $parsed->body)``), and compose re-verified it the same way (ADR-036 item 6); for a legacy row without a version digest it still does (``Classes/Service/Skill/SkillVersionDigest.php#hash('sha256', $skill->getBody())``). The sync's change test compared that value only, until item 1 below replaced it with the version digest. The same sync also rewrites ``name``, ``description``, ``allowed_tools``, ``support_status`` and ``raw_frontmatter`` (:php:`SkillSyncService::apply()`). An upstream change to the frontmatter alone is therefore an ``updated`` skill: it stays enabled, and nothing records that the text the model reads changed. ``allowed_tools`` decides which tools a run may call, ``name`` heads the composed section, and ``support_status`` decides whether asset references are stripped from the body (:php:`SkillComposer::renderSection()`). The ``body`` of ``tx_nrllm_skill`` is an editable text field, but ``body_checksum`` is read-only, so a body edit in the backend produces a skill that is skipped on every call. ``readOnly`` is a FormEngine setting only; no field of ``tx_nrllm_skill`` carries ``exclude``, so a DataHandler write by a group with ``tables_modify`` would reach ``trust_level``, ``body_checksum`` and ``enabled`` (:ref:`ADR-169 `, section 1). A sync overwrites the record in place, so no earlier version is kept. .. _adr-214-context-loading: 3. Every attached body is sent in full, and the tail is dropped --------------------------------------------------------------- Skills attach to a configuration and to a task through MM tables (ADR-036 item 8). The admin playground can force further skills onto one run (:php:`RunAugmentation`). A queued run persists those as uids only (:php:`AgentRunRequestCodec`), so the worker composes whatever body the record holds when it picks the run up. A suspended run replays the transcript stored in :php:`SuspendedRunState`, which already contains the composed text, and re-reads the forced skills by uid only for the ceiling check (:ref:`ADR-165 `). Nothing records which version of a skill a run was composed with. Every admitted body is composed in full. When the block exceeds ``skills.maxBytes`` (default 24 000), whole skills are dropped from the end, task skills first (ADR-036 item 5). There is no way to list a skill without sending its body, to load it when it is needed, or to invoke one explicitly. .. _adr-214-context-snippets: 4. Snippets do not reach a caller that brings its own system message -------------------------------------------------------------------- Prompt snippets (:ref:`ADR-031 `) are appended to the ``system_prompt`` option. :php:`MessageShaper::applySystemPrompt()` returns the message list unchanged when it already holds a system message (``Classes/Service/MessageShaper.php#if ($isSystem)``), and ADR-031 records this as intended: "a caller-supplied system *message* still suppresses the configuration's system prompt — and with it the snippet block". The chat of the ``nr_mcp_agent`` extension builds its own system prompt (``ChatService::buildSystemPrompt()``), which always starts with a fixed identity text, and ``ChatService::runAgentTurn()`` prepends it as a system message whenever it is not empty. Read together, the code says snippets never reach the chat. This is inferred from the code of both extensions; no run was measured. Neither ``tx_nrllm_promptsnippet`` nor ``tx_nrllm_configuration.system_prompt`` is restricted to administrators, and :ref:`ADR-169 ` section 2 recommends that non-administrators manage both tables. Whoever edits text that reaches a system message holds an instruction-level privilege. .. _adr-214-context-budget: 5. Text appended to an existing system message is not charged ------------------------------------------------------------- :php:`ContextWindowManager::fit()` charges the configuration's system prompt only when the first message is not a system message (:php:`ContextWindowManager::missingSystemPromptTokens()` returns 0 otherwise), plus the fenced skill block passed as ``$injectedText``. When the budget is exceeded, :php:`ContextWindowManager::drop()` removes the oldest whole turns (:ref:`ADR-107 `). Text that a later stage appends into an existing system message is not charged at all, and a larger head is paid for by evicting earlier turns, not by a refusal. .. _adr-214-context-tools: 6. The tool allow-list is resolved from the configuration alone, live --------------------------------------------------------------------- :php:`AllowedToolsResolver::resolve()` returns the union of the ``allowed-tools`` declarations of the effective skills, or ``null`` ("no restriction") when none declares. :php:`ToolCallPolicy` calls it with the configuration and no task (``Classes/Service/Tool/ToolCallPolicy.php#$this->allowedTools->resolve($configuration)``), in :php:`ToolCallPolicy::decide()` and in :php:`ToolCallPolicy::explain()`. Task skills and forced skills therefore restrict nothing at run time, although :ref:`ADR-038 ` item 5 names the task. A resume re-resolves the list live (:php:`ToolLoopService::resolveOfferedNames()`): if the only declaring skill is disabled while the run waits, the resumed run gets ``null``, which is every tool. The resolver's own docblock names the same widening for the budget drop and rejects it there. .. _adr-214-context-runs: 7. The run suspends for input or for approval, never for both ------------------------------------------------------------- A tool can suspend a run in ``WAITING_FOR_INPUT`` and resume it with input validated against the schema the tool declared (:ref:`ADR-105 `). No production tool implements :php:`RequiresInputInterface` today; only test fixtures do. A builtin that declares a write effect requires a human approval (:ref:`ADR-134 `), and the approval binds to the preview lines the approver saw: a resume recomputes them and bounces when they differ (:ref:`ADR-184 `). ADR-134 bans a write tool that also requires input, because the approval resume carries no input (:php:`ToolLoopService::resume()` takes a boolean). A denial feeds the model a fixed text that says who declined and nothing about why (``Classes/Service/Tool/ToolLoopService.php#private function approvalDeniedResult(``, :ref:`ADR-200 `). On a configuration with ``require_second_approver`` the run owner cannot approve their own write (:ref:`ADR-172 `; ``Classes/Controller/Backend/AgentRunController.php#runs.error.selfApproval``).