🔶 Cloud-owned — superseded in part by cloud ADR-0025 (2026-06-25). The in-UI AI runtime and the
ask/buildagents described here moved to the cloud / Enterprise distribution (@objectstack/service-ai→cloud/packages/service-ai, closed); the open framework exposes AI only via@objectstack/mcp(BYO-AI) and ships no in-productask/buildchat. Retained as historical design context, now cloud-owned — seecloud/docs/adr/0025-service-ai-to-cloud-open-mcp-only.
Status: Proposed (2026-06-22)
Deciders: ObjectStack Protocol Architects
Builds on: ADR-0063 (two agents bound by surface; skill ↔ agent affinity), ADR-0033 (draft-gated authoring tools)
Resolves: ADR-0040 §4 ("tools are registry-global; a custom agent can change persona but not constrain capability") — the named follow-up contract.
Consumers: @objectstack/service-ai (ToolRegistry, AgentRuntime), ../cloud/service-ai-studio (authoring tools), @objectstack/spec (no schema change required)
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.
With ADR-0063 settling two distinct agents bound by surface, the fix is structural, not a new permission system:
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.
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.
Findings from the 2026-06 ask/build assessment, confirmed across three packages:
- One global
ToolRegistryper AI service (service-ai/src/tools/tool-registry.ts);register()takes no agent/skill/surface argument. AgentRuntime.buildRequestOptions()composes an agent's tools fromagent.tools[]+ each active skill'stools[], deduped — but:actions_executorintentionally declares an emptytools: []and relies on a fall-through so the resolver hands it the wholeaction_*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.- Cloud authoring tools register globally (
service-ai-studio/src/plugin.ts);describe_object/list_objectsare explicitly commented as shared by both agents.
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.
AgentRuntime computes an agent's tool set strictly as:
tools(agent) = ⋃ { skill.tools | skill ∈ agent.skills ∧ skill.surface ∈ {agent.surface, 'both'} }
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:
actions_executordeclarestools: ['action_*']and the resolver expandsaction_*against registered tools — but only because the skill claims the pattern, not via fall-through. The empty-array + fall-through hack is deleted.
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:
describe_object,list_objects,query_dataare claimed by asurface:'both'schema_readerskill (or listed in bothdata_explorerand a build skill). They are read-only and safe to share.- Authoring/mutation tools (
create_metadata,apply_blueprint,add_field, …) are claimed only bysurface:'build'skills. They are therefore absent fromask's tool set on every deployment, and absent entirely on OSS where nobuildskill is registered.
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.
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.)
Positive
- Closes the ADR-0040 §4 hole structurally:
askcannot author because authoring tools are not in its set — no runtime guard to forget. - Single source of truth for "who can call what": the skill bundle. No tool-side scoping to drift.
- Shared read tools remain shared, explicitly, via a
surface:'both'reader skill.
Negative / costs
- Deletes the
actions_executorfall-through and the global-list reliance; any resolver path depending on fall-through must be migrated to explicit claims. - Requires auditing currently-global tools into surface-correct skills (esp. the shared read tools → a
bothreader skill). - A misfiled skill
surfacenow silently narrows or widens an agent's reach — sosurfacecorrectness is load-bearing and must be covered by tests.
- Remove the global fall-through in tool resolution; resolve wildcards against skill-claimed names only.
- Introduce a
surface:'both'schema_readerskill owningdescribe_object/list_objects/query_data; stop dual-listing by comment. - Make incompatible skill↔agent binding a fast load error in
resolveActiveSkills. - Tests: assert
tools(ask)contains nocreate_*/*_metadata/blueprint tools on cloud and OSS; assert authoring tools present only underbuild.