|
1 | | -# ADR-0109: The AI tool authoring model — third-party tools are bindings to executable primitives |
| 1 | +# ADR-0109: The AI tool authoring model — the default third-party path needs no tool records |
2 | 2 |
|
3 | | -**Status**: Proposed (2026-07-28) — the "D0" decision of issue #3820; blocks the `skill.tools` branch of the R7 reference-integrity rule. |
| 3 | +**Status**: Proposed (2026-07-28; revised same day — see Revision note). Phase 1 (platform tool-name registry + advisory reference lint) is implemented alongside this revision; Phase 2 (the optional refinement layer) awaits acceptance. |
4 | 4 | **Deciders**: ObjectStack Protocol Architects |
5 | | -**Builds on**: [ADR-0063](./0063-two-kernel-agents-skills-are-the-extension-primitive.md) (skills + tools are the third-party extension primitive), [ADR-0064](./0064-tool-scoping-to-agent.md) (an agent's tools are its skills' tools), [ADR-0078](./0078-no-silently-inert-metadata.md) (no silently inert metadata) |
6 | | -**Consumers**: `@objectstack/spec` (`ai/tool.zod.ts`, `stack.zod.ts`), `@objectstack/lint` (the R7 rule), `../cloud/service-ai` (ToolRegistry), `../cloud/service-ai-studio` |
| 5 | +**Builds on**: [ADR-0063](./0063-two-kernel-agents-skills-are-the-extension-primitive.md) (skills + tools are the third-party extension primitive), [ADR-0064](./0064-tool-scoping-to-agent.md) (an agent's tools are its skills' tools), [ADR-0078](./0078-no-silently-inert-metadata.md) (no silently inert metadata), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove) |
| 6 | +**Consumers**: `@objectstack/spec` (`system/constants/platform-tool-names.ts`, `ai/tool.zod.ts`, `stack.zod.ts`), `@objectstack/lint` (`validate-ai-tool-references`), `../cloud/service-ai` + `service-ai-studio` (registry conformance) |
| 7 | + |
| 8 | +> **Revision note.** The first draft framed third-party tools as *bindings to |
| 9 | +> actions/flows that authors declare as tool records*. Review surfaced the |
| 10 | +> stronger conclusion: the runtime **already materialises** a tool per |
| 11 | +> declarative action (`action_<name>`, the family `actions_executor` |
| 12 | +> subscribes to with `action_*`), so in the default path a third party needs |
| 13 | +> **no tool record at all** — the binding the draft proposed already exists |
| 14 | +> implicitly for every action. The tool record is thereby demoted from "the |
| 15 | +> authoring artifact" to an *optional refinement layer*, and Phase 1 shrinks |
| 16 | +> to what today's mechanisms already support. |
7 | 17 |
|
8 | 18 | --- |
9 | 19 |
|
10 | 20 | ## TL;DR |
11 | 21 |
|
12 | | -`stack.tools` today is a **declaration with no executable half and no reader**: `ToolSchema` carries no handler binding, the metadata `tool` type is written once at boot (Studio visibility mirror, `service-ai-studio/plugin.ts`) and read by **nothing**, `'tools'` is missing from `composeStacks`' `CONCAT_ARRAY_FIELDS`, and the executable tool set lives exclusively in the runtime-registered in-process `ToolRegistry`. ADR-0063 §2 says third parties extend via "skills **and tools**" — but only the skills half is real. That is the ADR-0078 prohibited state, and it makes any reference-integrity rule over `skill.tools[]` unbuildable: there is no registry of facts to resolve against (16/16 of HotCRM's skill→tool references miss `stack.tools`; 6 of those 16 nonetheless resolve — against the runtime registry lint cannot see). |
| 22 | +**Third parties extend AI by authoring skills — and only skills.** A skill's |
| 23 | +`tools[]` names either a platform-registered tool (curated in |
| 24 | +`PLATFORM_PROVIDED_TOOL_NAMES`) or a tool the runtime materialises from the |
| 25 | +app's own declarative actions (`action_<name>`). The executable, its authz, |
| 26 | +and its audit trail are the action/flow the app already ships for its UI — |
| 27 | +the AI can do exactly what the application can already do, under the same |
| 28 | +permissions, and nothing else. |
| 29 | + |
| 30 | +`stack.tools` records are an **optional AI-presentation refinement layer** |
| 31 | +(Phase 2): needed only when the AI-facing surface must differ from the raw |
| 32 | +executable. They are never required, and never a place business logic lives. |
13 | 33 |
|
14 | | -Decision: **a third-party tool is a *binding* to an executable primitive the platform already has — an action or a flow — never a free-floating executable of its own.** Kernel/plugin tools stay runtime-registered, and their names become facts via a curated, conformance-tested registry in spec. |
| 34 | +## Context |
| 35 | + |
| 36 | +`stack.tools` before this ADR was the ADR-0078 prohibited state: a |
| 37 | +declaration with no executable half and no reader. `ToolSchema` carries no |
| 38 | +handler binding; the metadata `tool` type is written once at boot (Studio |
| 39 | +visibility mirror) and read by nothing; `'tools'` was missing from |
| 40 | +`composeStacks`' concat list; the executable tool set lives exclusively in |
| 41 | +the runtime-registered in-process `ToolRegistry`. Meanwhile ADR-0063 §2 |
| 42 | +promised "skills **and tools**" as the third-party primitive — only the |
| 43 | +skills half was real. On the HotCRM corpus, 16/16 `skill.tools` references |
| 44 | +missed `stack.tools` while 6 of them resolved against the runtime registry |
| 45 | +lint could not see: no reference rule could be correct in either direction |
| 46 | +(#3820 D0/D2). |
15 | 47 |
|
16 | 48 | ## Decision |
17 | 49 |
|
18 | | -1. **Third-party tools bind, they don't implement.** `ToolSchema` gains a required-for-app-packages `binding` (`{ type: 'action' | 'flow', name: string }`). The runtime materialises a callable from the bound primitive (the same mechanism that already materialises `action_<name>` tools from object action lists); `parameters` may narrow, but never widen, what the bound primitive accepts. Handlers, authz, and audit stay where they already are — on the action/flow. A tool record without a `binding` is legal only for runtime-registered (kernel/plugin) tools, whose record is a visibility mirror, not a definition. |
19 | | -2. **Platform tool names become facts, not guesses.** Spec exports a curated `PLATFORM_PROVIDED_TOOL_NAMES: ReadonlySet<string>` (the `PLATFORM_PROVIDED_OBJECT_NAMES` / #3657 precedent), kept honest by conformance tests in the *owning* packages (service-ai / service-ai-studio assert "every tool I register is listed; every listed name with my prefix is registered"). The lint → spec dependency direction is preserved. |
20 | | -3. **Reference integrity becomes decidable, and R7's tool branch unblocks.** `skill.tools[]` (wildcard-aware) resolves against: bound tools declared in-stack (→ checkable through to the action/flow, which existing rules already resolve) ∪ `PLATFORM_PROVIDED_TOOL_NAMES` ∪ wildcard families. Resolution ladder per ADR-0072: unknown name → **warning** at first (ADR-0078 ratchet), **error** once the registry has soaked one release. |
21 | | -4. **The inert surface is closed.** `'tools'` joins `CONCAT_ARRAY_FIELDS`; the boot-time mirror writes gain provenance so a stack-declared record cannot shadow a platform tool's mirror (today `if (exists) skip` lets it); the runtime's "silently dropped" posture for unresolved references stays (graceful degradation) but logs, because the *authoring-time* signal now comes from lint — loud at the producer, tolerant at the consumer (Prime Directive #12). |
| 50 | +1. **The default path is skills-only.** A third party gives the AI a new |
| 51 | + capability by (a) declaring the executable as a normal action/flow — which |
| 52 | + the app needs for its UI anyway — and (b) authoring a skill whose |
| 53 | + `tools[]` names the materialised `action_<name>` tool or a platform tool. |
| 54 | + Zero tool records; permissions and audit are inherited from the executable |
| 55 | + by construction. This is also one less namespace an AI author can |
| 56 | + hallucinate into. |
| 57 | + |
| 58 | +2. **Platform tool names are facts, not guesses.** |
| 59 | + `PLATFORM_PROVIDED_TOOL_NAMES` in `@objectstack/spec` |
| 60 | + (`system/constants/platform-tool-names.ts`, the |
| 61 | + `PLATFORM_PROVIDED_OBJECT_NAMES` precedent) curates every statically-named |
| 62 | + tool the cloud AI runtime registers, grouped by owning package; |
| 63 | + `PLATFORM_TOOL_FAMILY_PREFIXES` names the dynamically-materialised |
| 64 | + families (today exactly `action_`). The owning cloud packages carry |
| 65 | + conformance tests — the same split as `CLOUD_PROVIDED_OBJECT_NAMES`, |
| 66 | + whose conformance half also lives outside this repo. |
| 67 | + |
| 68 | +3. **`skill.tools[]` reference integrity is decidable and linted** — |
| 69 | + `validate-ai-tool-references` in `@objectstack/lint`, a member of |
| 70 | + `REFERENCE_INTEGRITY_RULES`. Resolution universe: `stack.tools` names ∪ |
| 71 | + the platform registry ∪ the materialised `action_<name>` family (from |
| 72 | + `stack.actions` and every object's `actions`), wildcard-aware. Severity |
| 73 | + **warning** first (ADR-0078 ratchet): a runtime plugin outside the |
| 74 | + registry remains statically invisible, and the runtime deliberately |
| 75 | + tolerates unresolved names. On the HotCRM corpus the rule yields exactly |
| 76 | + the 10 fictional references and 0 false positives. |
| 77 | + |
| 78 | +4. **Tool records become the optional refinement layer (Phase 2).** A tool |
| 79 | + record earns its place only when the AI-facing surface must differ from |
| 80 | + the raw executable: LLM-directed description, parameter narrowing (expose |
| 81 | + 3 of 20 params, tighten enums), exposing a *flow* (no materialised family |
| 82 | + yet), a stable AI-facing name decoupled from the action's, or execution |
| 83 | + policy (a confirm-before-run flag — note `requiresConfirmation` was |
| 84 | + *removed* in #3715/#3876 as unenforced per ADR-0049; it returns only |
| 85 | + together with its enforcement, as part of this layer). When Phase 2 |
| 86 | + lands, a third-party tool record MUST carry a `binding` |
| 87 | + (`{ type: 'action' | 'flow', name }`) to the executable it refines — |
| 88 | + handlers never live on the tool. Until Phase 2 lands, third-party tool |
| 89 | + records remain inert-and-documented (the `stack.tools` doc says so), and |
| 90 | + the refinement needs above are the acceptance trigger. |
| 91 | + |
| 92 | +5. **The inert-surface holes close.** `'tools'` joins `composeStacks`' |
| 93 | + concat list (this revision — a declared record must at least survive |
| 94 | + composition). Phase 2 adds provenance to the boot-time metadata mirror so |
| 95 | + a stack-declared record cannot shadow a platform tool's entry (today |
| 96 | + `if (exists) skip` lets it), and revisits the `tool` metadata-type flags |
| 97 | + (`allowRuntimeCreate`/`allowOrgOverride`) which currently advertise an |
| 98 | + authoring surface with no reader. |
22 | 99 |
|
23 | | -## Rejected alternative |
| 100 | +## Rejected alternatives |
24 | 101 |
|
25 | | -**Tools as first-class executables** (a `handler` on ToolSchema): re-invents actions/flows inside the AI layer, splits authz/audit across two systems, and turns every tool into a second place to define business logic. The platform's ontology is already executable; the AI layer needs a *view* of it, not a rival. |
| 102 | +- **Tools as first-class executables** (a `handler` on ToolSchema): |
| 103 | + re-invents actions/flows inside the AI layer, splits authz/audit across two |
| 104 | + systems, and turns every tool into a second place to define business logic. |
| 105 | + The platform's ontology is already executable; the AI layer needs a *view* |
| 106 | + of it, not a rival. |
| 107 | +- **A required tool record per exposed action** (the first draft's implicit |
| 108 | + shape): the materialised `action_<name>` family already provides the |
| 109 | + binding implicitly, so a mandatory record would add a second authoring |
| 110 | + step, a second namespace to keep consistent, and a second surface for AI |
| 111 | + authors to hallucinate into — for zero added capability. |
26 | 112 |
|
27 | 113 | ## Consequences |
28 | 114 |
|
29 | | -- "Skills + tools" (ADR-0063 §2) becomes a shipped contract: a third party can author a skill whose tools are real, checkable, and governed by the same authz as the UI button that runs the same action. |
30 | | -- AI-authored metadata gets a closed vocabulary: a hallucinated tool name fails lint at draft time instead of shipping as a dead capability the copilot claims to have (the HotCRM failure, 10 fictional tools across 6 skills). |
31 | | -- Costs: a spec change (`binding`), the registry + conformance tests, a materialisation path in the runtime; the R7 tool-branch rule stays blocked until 1–2 land. |
| 115 | +- "Third-party AI extension = write a skill" becomes the whole story — one |
| 116 | + concept to document, teach, and generate. ADR-0063 §2's "skills and tools" |
| 117 | + reads as: skills are the primitive, tools are its optional refinement. |
| 118 | +- AI capability ≡ application capability: same executables, same |
| 119 | + permissions, same audit. There is no second security system to review and |
| 120 | + no way for the AI to do what the app cannot. |
| 121 | +- A hallucinated tool name fails lint at draft time (advisory now, gating |
| 122 | + after the ratchet) instead of shipping as a copilot that claims abilities |
| 123 | + it does not have — the HotCRM failure, 10 fictional tools across 6 skills. |
| 124 | +- Cost honestly stated: the registry is a curated list that can go stale; |
| 125 | + the conformance tests in the owning packages are the mechanism that keeps |
| 126 | + it true, and they live in a different repository than the list. |
32 | 127 |
|
33 | 128 | ## Follow-up |
34 | 129 |
|
35 | | -- [ ] `binding` on `ToolSchema` + `'tools'` into `CONCAT_ARRAY_FIELDS` (spec). |
36 | | -- [ ] `PLATFORM_PROVIDED_TOOL_NAMES` + owning-package conformance tests (spec, cloud). |
37 | | -- [ ] Materialise bound tools in the ToolRegistry; provenance-guard the boot mirror (cloud). |
38 | | -- [ ] R7 tool branch: `validate-ai-references` skill→tool resolution, warning-first (lint; #3820). |
| 130 | +- [x] `PLATFORM_PROVIDED_TOOL_NAMES` + `PLATFORM_TOOL_FAMILY_PREFIXES` + invariant tests (spec — this change). |
| 131 | +- [x] `validate-ai-tool-references`, advisory, in `REFERENCE_INTEGRITY_RULES` (lint — this change). |
| 132 | +- [x] `'tools'` into `composeStacks`' concat list (spec — this change). |
| 133 | +- [ ] Registry conformance tests in `../cloud` `service-ai` / `service-ai-studio` (cloud PR; activates fully when the cloud `.objectstack-sha` pin advances past this change). |
| 134 | +- [ ] **Phase 2 (on acceptance + a real refinement need):** `binding` on `ToolSchema`; flow exposure; boot-mirror provenance guard; revisit `tool` metadata-type flags; ratchet the lint rule to error per ADR-0078 once the registry has soaked a release. |
0 commit comments