Skip to content

Commit 5d2b00a

Browse files
committed
2 parents 47e3568 + 3d89caa commit 5d2b00a

6 files changed

Lines changed: 359 additions & 0 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
Fix: the Setup-nav capability gate (`requiresService`, ADR-0057 D10) was a no-op on the single-item app-meta path.
6+
7+
`GET /meta/app/:name` returns a metadata envelope `{ type, name, item: <app>, ... }`, but
8+
`filterAppForUser` was applied to the envelope — whose `.navigation` is undefined — so it
9+
returned it untouched, silently bypassing BOTH the `requiredPermissions` gate and the D10
10+
`requiresService` gate. Organizations/Invitations therefore still appeared in the Setup app
11+
even in single-tenant deployments. `filterAppForUser` and `resolveRegisteredServices` now
12+
unwrap the envelope (the list path already passed the raw app). Verified against a live
13+
`os dev`: single-tenant hides Organizations/Invitations; multi-tenant shows them.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# ADR-0063: Two agents (`ask` / `build`), bound by surface; skills are the only third-party extension primitive
2+
3+
**Status**: Proposed (2026-06-22)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Supersedes**: [ADR-0040](./0040-unified-assistant-and-agent-binding.md) — its core decision (a *single* unified assistant selected by *per-turn intent classification*) is **reversed**. ADR-0040's UX win ("the end user never picks from a roster") is **kept** but re-grounded: the *surface* binds the agent, not a classifier and not a dropdown. §3 (custom tenant agents) is withdrawn; §4 (tool-scoping) is handed to [ADR-0064](./0064-tool-scoping-to-agent.md).
6+
**Builds on**: [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (draft → verify → publish), [ADR-0038](./0038-build-verification-loop.md) (verify-fix-reverify discipline carried by skills)
7+
**Consumers**: `@objectstack/spec` (agent/skill types), `@objectstack/service-ai` (the open-source `ask` agent), `../cloud/service-ai-studio` (the cloud-only `build` agent), `../objectui` (chat surfaces)
8+
9+
**Premise**: pre-launch, no back-compat debt beyond the alias table — specify the target end-state directly.
10+
11+
> **Revises** the first cut of ADR-0063 (merged in #2164), which framed the two as "shells split by what they mutate" while leaving ADR-0040's unified assistant in place. On reflection the unified assistant was itself the error; this revision separates the two agents by *surface*.
12+
13+
---
14+
15+
## TL;DR
16+
17+
The kernel ships **exactly two agents**, and they are **two products for two different needs**, not two intents of one assistant:
18+
19+
- **`ask`** — the data product (≈ **Claude Chat**). Conversational read/query/explore over records, and execution of business actions. End-user audience, RLS-bounded, fast turns. **Open-source, free** (`@objectstack/service-ai`).
20+
- **`build`** — the authoring product (≈ **Claude Code**). Agentic mutation of *metadata* (objects, fields, views, flows) through a plan → draft → verify → publish loop. Builder/admin audience, governance-gated, long-running pinned sessions. **Cloud-only, paid** (`../cloud/service-ai-studio`).
21+
22+
**The user never picks an agent from a roster — the surface they are in binds it.** You are in the data console → `ask`; you are in the builder/Studio → `build`. Exactly as you open *claude.ai* or you open *Claude Code* — you choose a product, not an agent.
23+
24+
Everything else — domain depth, playbooks, persona, judgment, reach to external systems — is delivered as **skills** (+ tools / MCP), each bound to one agent by affinity. **Third parties extend the platform by authoring `*.skill.ts` and tools, never `*.agent.ts`.** Claude Code is enormously capable and ships **zero** user-authored agents — its power is skills + tools/MCP + platform-owned subagents. ObjectStack adopts the same shape.
25+
26+
---
27+
28+
## Context — why ADR-0040's unified assistant was the wrong call
29+
30+
ADR-0040 collapsed data Q&A and metadata authoring into **one** assistant carrying *all* skills, switched by a **per-turn intent classifier**. On reflection that was a design error, for the same reason Anthropic ships Claude Chat and Claude Code as **separate products**: they serve genuinely different needs, with different interaction models, audiences, risk profiles, and pricing.
31+
32+
| | `ask` (≈ Chat) | `build` (≈ Code) |
33+
|---|---|---|
34+
| Need | conversational data Q&A + act | agentic authoring of the app itself |
35+
| Audience / permission | end user · row-level security | builder/admin · governance gate |
36+
| Mutates | records (business data) | metadata (the app definition) |
37+
| Interaction rhythm | quick question → answer | plan → draft → verify → publish, self-correcting, long-running |
38+
| Blast radius | one user's data, RLS-scoped | the app for everyone |
39+
| Commerce | open-source · free | cloud-only · paid |
40+
41+
These are not two *intents* of one assistant — they are two *products*. The seam (**"what you change": records vs. the app definition**) carries the architectural boundary, the governance boundary, **and** the commercial boundary at once: three lines, one cut.
42+
43+
**ADR-0040's own incident argues for separation, not unification.** Its motivating failure (staging, 2026-06-11) was a user in the *Data* assistant asking it to "build a library app"; it flailed because it lacked the authoring disciplines. ADR-0040 read this as "the data persona was missing build skills" and bolted all skills onto one assistant. The correct reading: **that request should never have reached the data surface.** In a Chat/Code-separated world, app-building happens in the builder surface. The incident is evidence the *surfaces were leaking*, not evidence for merging the personas. The per-turn classifier ADR-0040 introduced is a fragile router that re-implements — worse, and at the wrong layer — the boundary a clean product split gives for free (misclassification, wrong-discipline leakage, mid-conversation handoff).
44+
45+
**What ADR-0040 got right, and we keep:** the end user must not face a roster/dropdown of agents. We preserve that — but the resolution is **surface binding**, not a classifier and not a menu.
46+
47+
---
48+
49+
## Decision
50+
51+
### 1. Two agents, two products, bound by surface
52+
53+
The two agents above are platform-owned. The agent is resolved deterministically from **where the user is**, never chosen per-turn or from a roster:
54+
55+
- Data console / embedded app chat → **`ask`**.
56+
- Builder / Studio authoring surface → **`build`**.
57+
- Resolution chain stays `app.defaultAgent` → surface default → platform default; the surface sets the default, so the user makes **no** selection.
58+
- No per-turn intent classifier. A `build`-shaped request arriving at an `ask` surface is **declined and redirected to the builder**, not silently re-routed into authoring.
59+
60+
Why exactly two: merging them blurs the records-vs-app-definition governance boundary (different blast radius, audience, verification discipline, price); splitting further re-creates the roster this ADR refuses.
61+
62+
### 2. Skills (+ tools / MCP) are the only third-party extension primitive
63+
64+
- The platform is metadata-driven, so domain *structure* is already shared: both agents read the full metadata registry (objects, fields, actions, flows) and are structurally domain-aware for free. "A custom agent is needed to teach the assistant the industry" is false — the ontology is in the metadata.
65+
- What is left over — persona, judgment, playbooks, instructions, bundled tools, external reach — is **exactly the definition of a skill**. Calling that a "custom agent" is using the wrong word for a skill.
66+
- **Skills compose; agents don't.** A real request can span domains (CRM + accounting + an ISV connector); skill-first loads every relevant skill into the same turn, agent-first forces one. **Skills scale; agents don't** — progressive disclosure makes 1,000 skills cost ~nothing until relevant; 1,000 agents is a selection nightmare.
67+
- Therefore: third parties author `*.skill.ts` and tools / MCP connectors. `*.agent.ts` is **closed to third parties** — internal only, for the two platform agents and platform-owned subagents (the `Explore`/`Plan` analogues). **ADR-0040 §3 (tenant custom agents bound via `app.defaultAgent`) is withdrawn.**
68+
69+
### 3. Skill ↔ agent affinity (the one new contract)
70+
71+
Because the two agents are distinct, each skill declares which one it belongs to:
72+
73+
```ts
74+
defineSkill({
75+
name: 'churn_analysis',
76+
surface: 'ask', // 'ask' | 'build' | 'both'
77+
// …instructions, tools
78+
})
79+
```
80+
81+
- `data_explorer`, `actions_executor``ask`
82+
- `metadata_authoring`, `solution_design``build`
83+
- A cross-cutting ISV domain skill → usually `ask`, occasionally `both`
84+
85+
This is the analogue of a Claude Code skill being scoped to the Code surface. Affinity is also what makes tool scoping clean (ADR-0064): an agent's tool set is the union of its skills' tools, and a skill cannot attach to an agent whose surface it does not match.
86+
87+
### 4. Naming — one canonical word per agent, by user intent
88+
89+
| | data agent | authoring agent |
90+
|---|---|---|
91+
| **Canonical id / spoken name** | **`ask`** | **`build`** |
92+
| Legacy alias (permanent, silent — back-compat only) | `data_chat` | `metadata_assistant` |
93+
| User-facing label (may stay friendly) | "Assistant" | "Builder" |
94+
95+
Rules:
96+
97+
- Name by **user intent (a verb)**: `ask` + `build` share one axis ("ask my app" / "build my app"). `data` + `build` would mix a noun with a verb and the ambiguity grows back.
98+
- `ask` over `data` because the agent also executes actions — "ask → answer → act"; "data" implies read-only.
99+
- The commercial line reads off the names: **Ask your app (free) / Build your app (paid).**
100+
- `data_chat` / `metadata_assistant` are **aliases, never vocabulary**; docs and code use `ask` / `build` exclusively.
101+
- **`build` is not yet canonical in code** — the cloud agent id is still `metadata_assistant`. Make `build` canonical there, mirroring the `ask` rename. No further id churn after that.
102+
103+
### 5. Surface binding, not per-turn routing; tier degradation
104+
105+
- The **surface** picks the agent (§1). There is no per-turn intent classifier to maintain, mis-tune, or mis-fire.
106+
- On the **open-source / free** deployment only `ask` (and its `surface:'ask'` skills) exists. The builder surface and `build` agent simply aren't present; an `ask`-surface user who asks to build the app is told app-building lives in the (cloud) Builder, with no half-built attempt.
107+
- **`build`-affinity skills only light up where the `build` agent exists** (cloud). A `surface:'build'` skill is inert on OSS by design — documented at the authoring contract, not a bug.
108+
- Therefore the **free `ask` agent + open `ask`-skill ecosystem must stand on its own.** The free-tier value story rests entirely on it.
109+
110+
---
111+
112+
## Consequences
113+
114+
**Positive**
115+
- One mental model: two products, bound by surface; everything else is a skill. The "agent zoo" / choice-confusion failure mode is designed out, and so is the fragile per-turn classifier.
116+
- The capability-boundary gap ADR-0040 §4 worried about narrows sharply: there is no tenant agent to constrain, and an agent's tools are exactly its skills' tools. Residual cross-agent sharing (read-only `describe_object` / `list_objects`) is handled by ADR-0064.
117+
- Architecture, governance, and pricing align on a single seam.
118+
119+
**Negative / costs**
120+
- Reverses ADR-0040's core: the unified all-skills `ask`, the per-turn intent classifier, and the `buildRegisterActive` degradation shim are removed (see cleanup).
121+
- Withdraws tenant custom agents: flip the `agent` metadata-type flags and filter the runtime catalog.
122+
- Adds the `surface` affinity field to the skill schema and the scoping work in ADR-0064.
123+
124+
---
125+
126+
## Follow-up work (tracked in the consolidated issue)
127+
128+
Implementation, none of which changes a *public* id:
129+
130+
- [ ] **Split the personas.** Remove the unified `ask`-carries-all-skills definition and the per-turn intent preamble; `ask` carries only `surface:'ask'` skills, `build` only `surface:'build'`. Delete the `buildRegisterActive` degradation shim in `agent-runtime.ts` (no longer needed once surfaces are separate).
131+
- [ ] **Make `build` canonical** in `../cloud/service-ai-studio` (`metadata_assistant``build`), registering `metadata_assistant` as the permanent alias — mirroring `data_chat``ask`.
132+
- [ ] **Finish the `ask` rename drift**: `service-ai/src/agents/data-chat-agent.ts``ask-agent.ts`; `DEFAULT_DATA_AGENT_NAME``ASK_AGENT_NAME` (keep `LEGACY_DATA_AGENT_NAME`). Prose sweep "Data/Metadata Assistant" → `ask`/`build`.
133+
- [ ] **Withdraw tenant agents**: set `agent` metadata-type `allowRuntimeCreate:false, allowOrgOverride:false` in `metadata-plugin.zod.ts`; filter custom agents from the runtime catalog; drop the `app.defaultAgent` custom-agent guidance in the cloud plugin.
134+
- [ ] **Add `surface: 'ask' | 'build' | 'both'`** to `SkillSchema`; backfill the four built-in skills.
135+
- [ ] **Bind agent → surface** in `../objectui` (data console → `ask`, Studio → `build`); keep the picker hidden/builder-only.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# ADR-0064: Tool scoping — an agent's tools are exactly its skills' tools
2+
3+
**Status**: Proposed (2026-06-22)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0063](./0063-two-kernel-agents-skills-are-the-extension-primitive.md) (two agents bound by surface; skill ↔ agent affinity), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (draft-gated authoring tools)
6+
**Resolves**: [ADR-0040 §4](./0040-unified-assistant-and-agent-binding.md) ("tools are registry-global; a custom agent can change persona but not constrain capability") — the named follow-up contract.
7+
**Consumers**: `@objectstack/service-ai` (ToolRegistry, AgentRuntime), `../cloud/service-ai-studio` (authoring tools), `@objectstack/spec` (no schema change required)
8+
9+
---
10+
11+
## TL;DR
12+
13+
Today every agent can see every registered tool: the `ToolRegistry` is **global**, and the `actions_executor` skill deliberately **falls through** to the global list. That is the ADR-0040 §4 gap — the reason a data assistant *could* author metadata.
14+
15+
With [ADR-0063](./0063-two-kernel-agents-skills-are-the-extension-primitive.md) settling **two distinct agents bound by surface**, the fix is structural, not a new permission system:
16+
17+
> **An agent's available tools = the union of the tools declared by the skills bound to that agent. Nothing falls through to the global registry.**
18+
19+
The registry stays global for *registration*; *resolution* is scoped by the agent's skill bundle, which is itself scoped by `surface` affinity. `ask` literally has no authoring tools in its tool set, so it cannot author — by construction, not by a runtime check.
20+
21+
---
22+
23+
## Context
24+
25+
Findings from the 2026-06 ask/build assessment, confirmed across three packages:
26+
27+
- **One global `ToolRegistry` per AI service** (`service-ai/src/tools/tool-registry.ts`); `register()` takes no agent/skill/surface argument.
28+
- **`AgentRuntime.buildRequestOptions()`** composes an agent's tools from `agent.tools[]` + each active skill's `tools[]`, deduped — *but*:
29+
- **`actions_executor` intentionally declares an empty `tools: []` and relies on a fall-through** so the resolver hands it the whole `action_*` set from the global registry (`service-ai/src/skills/actions-executor-skill.ts`). This fall-through is the hole: any agent loading that skill — or any resolver bug — exposes the global list.
30+
- **Cloud authoring tools register globally** (`service-ai-studio/src/plugin.ts`); `describe_object` / `list_objects` are explicitly commented as shared by both agents.
31+
32+
So scoping exists only by convention (which skills an agent happens to carry), with a deliberate bypass. ADR-0040 §4 flagged this; ADR-0063 makes it fixable cleanly because there is now no tenant agent and each skill has a single-surface home.
33+
34+
## Decision
35+
36+
### 1. Resolution is closed over the skill bundle
37+
38+
`AgentRuntime` computes an agent's tool set strictly as:
39+
40+
```
41+
tools(agent) = ⋃ { skill.tools | skill ∈ agent.skills ∧ skill.surface ∈ {agent.surface, 'both'} }
42+
```
43+
44+
No tool reaches the model unless a bound, surface-compatible skill names it. **Remove the global fall-through.** Wildcards stay, but resolve against the registry *filtered to the names a skill claims*, not the whole registry:
45+
46+
- `actions_executor` declares `tools: ['action_*']` and the resolver expands `action_*` against registered tools — but only because the skill *claims* the pattern, not via fall-through. The empty-array + fall-through hack is deleted.
47+
48+
### 2. Registry stays global; a tool may be claimed by many skills
49+
50+
Tools are still registered once on the shared registry (no per-agent registration, no duplication). Scoping is a *read-time filter*, not a partitioned store. A tool can be claimed by skills on different surfaces — that is how genuinely shared read tools work:
51+
52+
- `describe_object`, `list_objects`, `query_data` are claimed by a `surface:'both'` **`schema_reader`** skill (or listed in both `data_explorer` and a build skill). They are read-only and safe to share.
53+
- Authoring/mutation tools (`create_metadata`, `apply_blueprint`, `add_field`, …) are claimed **only** by `surface:'build'` skills. They are therefore absent from `ask`'s tool set on every deployment, and absent entirely on OSS where no `build` skill is registered.
54+
55+
### 3. Affinity is enforced at bind time
56+
57+
A skill whose `surface` is incompatible with an agent is a **load error**, not a silent drop: binding a `surface:'build'` skill to `ask` fails fast in `resolveActiveSkills`. This makes "ask can't author" a checked invariant, not an emergent property.
58+
59+
### 4. No new schema
60+
61+
This needs no field beyond ADR-0063's `skill.surface`. `ToolSchema` gains **no** `scopedToSurface` field — scoping is derived (tool ← skill ← agent), not declared on the tool. Declaring it on the tool too would create a second source of truth that can disagree with the skill bundle. (Tool-level `permissions` for RLS/role checks is orthogonal and stays.)
62+
63+
## Consequences
64+
65+
**Positive**
66+
- Closes the ADR-0040 §4 hole structurally: `ask` cannot author because authoring tools are not in its set — no runtime guard to forget.
67+
- Single source of truth for "who can call what": the skill bundle. No tool-side scoping to drift.
68+
- Shared read tools remain shared, explicitly, via a `surface:'both'` reader skill.
69+
70+
**Negative / costs**
71+
- Deletes the `actions_executor` fall-through and the global-list reliance; any resolver path depending on fall-through must be migrated to explicit claims.
72+
- Requires auditing currently-global tools into surface-correct skills (esp. the shared read tools → a `both` reader skill).
73+
- A misfiled skill `surface` now silently narrows or widens an agent's reach — so `surface` correctness is load-bearing and must be covered by tests.
74+
75+
## Follow-up work (consolidated issue)
76+
77+
- [ ] Remove the global fall-through in tool resolution; resolve wildcards against skill-claimed names only.
78+
- [ ] Introduce a `surface:'both'` `schema_reader` skill owning `describe_object`/`list_objects`/`query_data`; stop dual-listing by comment.
79+
- [ ] Make incompatible skill↔agent binding a fast load error in `resolveActiveSkills`.
80+
- [ ] Tests: assert `tools(ask)` contains no `create_*`/`*_metadata`/blueprint tools on cloud and OSS; assert authoring tools present only under `build`.

0 commit comments

Comments
 (0)