Skip to content

Commit 259af21

Browse files
os-zhuangclaude
andauthored
feat(spec,lint): ADR-0109 revised + Phase 1 — skills-only default path, tool-name registry, advisory skill.tools lint (#3820 R7) (#3885)
ADR-0109 revision: the first draft framed third-party tools as authored binding records; review surfaced the stronger conclusion — the runtime already materialises a tool per declarative action (action_<name>), so the DEFAULT third-party path needs no tool records at all. Skills are the only required authoring artifact; tool records are demoted to an optional AI-presentation refinement layer (Phase 2, gated on acceptance and a real refinement need). AI capability ≡ application capability: same executable, same authz, same audit. Phase 1, implemented here: - spec: PLATFORM_PROVIDED_TOOL_NAMES / PLATFORM_TOOL_FAMILY_PREFIXES / isPlatformProvidedToolName in system/constants/platform-tool-names.ts — the PLATFORM_PROVIDED_OBJECT_NAMES precedent applied to tools. Owning packages live in the cloud repo, so (like CLOUD_PROVIDED_OBJECT_NAMES) the conformance half of the contract lives there; this repo pins the list's internal invariants. - lint: validate-ai-tool-references — the #3820 R7 skill.tools branch, wildcard-aware, resolving against stack.tools ∪ registry ∪ materialised action_<name> family. Warning severity (ADR-0078 advisory-first): runtime plugins outside the registry are statically invisible. Appended to REFERENCE_INTEGRITY_RULES → validate/lint/compile pick it up. Exactly 10 findings / 0 false positives on the HotCRM corpus; a dedicated near-miss suggestion catches the raw-action-name-instead-of-action_<name> mistake edit distance cannot. - spec: 'tools' joins composeStacks' CONCAT_ARRAY_FIELDS (declared records survive composition) and the stack.tools / AI-slot docs teach the ADR-0109 model. Claude-Session: https://claude.ai/code/session_01BHjroNkLkajskKbJaidko4 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 93f267f commit 259af21

12 files changed

Lines changed: 655 additions & 28 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": minor
4+
---
5+
6+
feat(spec,lint): ADR-0109 Phase 1 — platform tool-name registry + advisory `skill.tools[]` reference lint (#3820 R7)
7+
8+
ADR-0109 (revised) settles the AI tool authoring model: **the default
9+
third-party path needs no tool records at all.** A skill's `tools[]` names
10+
either a platform-registered tool or a tool the runtime materialises from the
11+
app's own declarative actions (`action_<name>`) — the executable, its authz,
12+
and its audit trail stay on the action/flow the app already ships. Tool
13+
records are demoted to an optional AI-presentation refinement layer (Phase 2,
14+
gated on acceptance).
15+
16+
Phase 1, shipped here:
17+
18+
- **`PLATFORM_PROVIDED_TOOL_NAMES`** (`@objectstack/spec/system`) — curated
19+
registry of every statically-named tool the cloud AI runtime registers,
20+
grouped by owning package, plus `PLATFORM_TOOL_FAMILY_PREFIXES` for the
21+
materialised `action_` family and `isPlatformProvidedToolName()`. The
22+
`PLATFORM_PROVIDED_OBJECT_NAMES` precedent, applied to tools; conformance
23+
tests live in the owning cloud packages.
24+
- **`validate-ai-tool-references`** (`@objectstack/lint`) — the #3820 R7
25+
`skill.tools` branch, wildcard-aware, resolving against declared
26+
`stack.tools` ∪ the registry ∪ the materialised action family. Severity
27+
**warning** (ADR-0078 advisory-first ratchet): the registry cannot see
28+
third-party runtime plugins. Joins `REFERENCE_INTEGRITY_RULES`, so
29+
`validate`, `lint`, and `compile` all pick it up. On the HotCRM corpus it
30+
reports exactly the 10 fictional tool references (0 false positives on the
31+
6 that resolve).
32+
- **`composeStacks` no longer drops `tools`** — the slot joins the
33+
concatenated array fields, so a declared record survives composition.
34+
- `stack.tools` / AI-slot docs updated to the ADR-0109 model.
Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,134 @@
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
22

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.
44
**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.
717
818
---
919

1020
## TL;DR
1121

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.
1333

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).
1547

1648
## Decision
1749

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.
2299

23-
## Rejected alternative
100+
## Rejected alternatives
24101

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.
26112

27113
## Consequences
28114

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.
32127

33128
## Follow-up
34129

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.

packages/lint/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,15 @@ export type {
231231
AiSurfaceAffinitySeverity,
232232
} from './validate-ai-surface-affinity.js';
233233

234+
export {
235+
validateAiToolReferences,
236+
AI_SKILL_TOOL_UNRESOLVED,
237+
} from './validate-ai-tool-references.js';
238+
export type {
239+
AiToolRefFinding,
240+
AiToolRefSeverity,
241+
} from './validate-ai-tool-references.js';
242+
234243
// One entry point for the reference-resolution rules above (#3583 §5 D5).
235244
// Adding a rule to `REFERENCE_INTEGRITY_RULES` runs it on `validate`, `lint`
236245
// and `compile` at once — the CLI call sites do not change.

packages/lint/src/reference-integrity-suite.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('reference-integrity suite — membership', () => {
2323
'validateTranslationReferences',
2424
'validateFlowTemplatePaths',
2525
'validateAiSurfaceAffinity',
26+
'validateAiToolReferences',
2627
]);
2728
});
2829

@@ -111,7 +112,9 @@ describe('reference-integrity suite — every member actually runs', () => {
111112
// validateAiSurfaceAffinity: an 'ask' agent binding a 'build' skill — the
112113
// runtime throws on this at chat time (ADR-0064 §3).
113114
agents: [{ name: 'helper', surface: 'ask', skills: ['metadata_authoring'] }],
114-
skills: [{ name: 'metadata_authoring', surface: 'build', tools: [] }],
115+
// validateAiToolReferences: a tool name nothing declares, registers, or
116+
// materialises (the HotCRM fictional-tool class).
117+
skills: [{ name: 'metadata_authoring', surface: 'build', tools: ['forecast_revenue'] }],
115118
flows: [
116119
{
117120
name: 'lead_followup',
@@ -145,6 +148,7 @@ describe('reference-integrity suite — every member actually runs', () => {
145148
expect(rules).toContain('translation-target-unknown');
146149
expect(rules).toContain('flow-template-unknown-field');
147150
expect(rules).toContain('ai-skill-surface-mismatch');
151+
expect(rules).toContain('ai-skill-tool-unresolved');
148152
});
149153

150154
it('carries a gating flow-template finding through the suite (#3810)', () => {
@@ -166,9 +170,9 @@ describe('reference-integrity suite — every member actually runs', () => {
166170
expect(typeof f.message).toBe('string');
167171
expect(typeof f.hint).toBe('string');
168172
}
169-
// Object references run first, AI surface affinity last.
173+
// Object references run first, AI tool references last.
170174
expect(findings[0].rule).toBe('object-reference-unknown');
171-
expect(findings[findings.length - 1].rule).toBe('ai-skill-surface-mismatch');
175+
expect(findings[findings.length - 1].rule).toBe('ai-skill-tool-unresolved');
172176
});
173177

174178
it('returns nothing for an empty stack', () => {

packages/lint/src/reference-integrity-suite.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { validateNavAccess } from './validate-nav-access.js';
5353
import { validateTranslationReferences } from './validate-translation-references.js';
5454
import { validateFlowTemplatePaths } from './validate-flow-template-paths.js';
5555
import { validateAiSurfaceAffinity } from './validate-ai-surface-affinity.js';
56+
import { validateAiToolReferences } from './validate-ai-tool-references.js';
5657

5758
export type ReferenceIntegritySeverity = 'error' | 'warning';
5859

@@ -96,6 +97,7 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
9697
{ name: 'validateTranslationReferences', run: validateTranslationReferences },
9798
{ name: 'validateFlowTemplatePaths', run: validateFlowTemplatePaths },
9899
{ name: 'validateAiSurfaceAffinity', run: validateAiSurfaceAffinity },
100+
{ name: 'validateAiToolReferences', run: validateAiToolReferences },
99101
];
100102

101103
/**

0 commit comments

Comments
 (0)