Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/30803-bare-name-ai-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@objectstack/objectql': minor
---

feat(objectql)!: an agent and a skill hold one name per package — once the cold boot has sealed the registry, a package declaring one under a name another installed package holds is refused at install and registration, naming both holders and the rename; a boot that already holds such a clash warns. This closes phase 1 of the bare-name series.

Clause-②: no (narrowing) — the engine's package door (`SchemaRegistry.installPackage`) and item seam (`SchemaRegistry.registerItem`) refuse, after the cold boot, a package registration they accepted before: one whose agent or skill bears a name another installed package holds. Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — `skills/**` does not teach that two packages may share an agent or skill name, and the naming rule itself is phase 2's (#30805); the prescription ships in the refusal and warning text and in ADR-0048's 2026-11-06 addendum (P1.9).

<!-- adr-0087: not-required (no-migration-prescription) the refusal removes no key, export or field and changes the shape of no stored body; what an author does about a refused name is pick a different one, which no conversion can choose for them -->

**BREAKING** accept-set narrowing at the package registration door, the fourth and last family of maintainer ruling C on #30798, phase 1 (picklists, mappings and datasets; then APIs, object-less actions and functions; then flows, webhooks, connectors and email templates): managed install (`install-local`, a post-start `manifest.register`, a hot reload) and package registration (`ObjectQL.registerApp` and every package-bound `registerItem`), for agents and skills. ADR-0048 §3.4 narrowed (addendum 2026-11-06, P1.9).

- **Why.** Measured on a booted showcase with two packages sharing one agent name and one skill name, in both registration orders: `/meta/agent/NAME` served the first-registered package's agent, and an MCP client's `prompts/get` of the skill resolved the first-registered package's skill (`listSkillPrompts` keeps the first row per name), as did `/meta/skill/NAME`. Each answer flipped with registration order, and neither warned. A third package installed after the boot under either name installed (`200`) and was shadowed.
- **Before.** Two installed packages could each ship an agent or a skill under one name, at boot or by a later install; every caller was served one of them, chosen by registration order.
- **Now.** After the cold boot (the engine plugin seals the registry at `kernel:bootstrapped`), a package registration declaring an agent or a skill under a name another installed package holds is refused ahead of every mutation, so it leaves no record behind. The envelope is the first family's: `code: 'NAMESPACE_CONFLICT'`, `status: 422`, `install-local` answering `422 PLUGIN_REGISTER_FAILED`, every conflicting name in one refusal, both holders named, the rename (the incoming package's namespace prefixed) carried, `conflicts[]` structured.
- **The cold boot.** A deployment whose installed packages already share such a name still boots: the one warning line names every such name, both holders and the rename, and changes nothing about which holder each caller is served (maintainer ruling 2026-11-06: the boot warns, only a new holder is refused; `BARE_NAME_COLD_BOOT_MODE` stays `'warn'`).
- **Who moves.** An author whose package declares an agent or a skill under a name another package installed beside it already ships, at the next install or upgrade into a running deployment. **The one-line fix: rename the item in one of the two packages — the refusal names the namespace-prefixed name — or uninstall one of them.** An MCP client, an app or a skill that named the old name must name the new one; nothing rewrites stored references.
- **Unchanged.** The same package registering its own name again. An environment item under a package's name (never judged, never a holder: the ADR-0005 overlay). One package's agent and another package's skill under one name (two types). A `tool` and a `rag_pipeline`, which the ruling does not list, and every other type the rule does not list (ADR-0048 §3.4 as written). `OS_METADATA_COLLISION=warn` does not downgrade the refusal.

**Measured producers.** On objectstack `30e0bd16`, no example app and no platform package declares an agent or a skill (`app-showcase`'s coverage file defers both, ADR-0063), so no shipped name has two holders. HotCRM is not checked out in this container and is NOT MEASURED here. Deployed and marketplace packages NOT MEASURED.
71 changes: 53 additions & 18 deletions docs/adr/0048-cross-package-metadata-collision.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package as first-class citizen), [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay precedence), [ADR-0008](./0008-metadata-repository-and-change-log.md) (metadata repository, `MetaRef` identity), [ADR-0010](./0010-metadata-protection-model.md) (package provenance / `_packageId` stamping)
**Consumers**: `@objectstack/objectql` (`SchemaRegistry.registerItem`, `ObjectQL.registerApp`, install path), `@objectstack/console` / `objectui` (routing + metadata resolution), package authors, CLI/CI install path, the app marketplace registry.
**Surfaced by**: ADR-0046 review (doc naming) → generalised to all bare-named metadata → re-examined through the app-marketplace install lens.
**Addenda**: [2026-08-08 — publish-time / marketplace namespace exclusivity](#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) (**Accepted** 2026-08-08 — maintainer ruling on #1825, [acceptance comment 5226411687](https://github.com/objectstack-ai/objectstack/issues/1825#issuecomment-5226411687), landed by PR #6671; additive to §3.2, supersedes nothing); [2026-11-06 — bare names resolved without a package hold one package](#addendum-2026-11-06-bare-names-resolved-without-a-package-hold-one-package--34-narrowed) (**Accepted**, ruling C on #30798, phase 1; narrows §3.4 for picklists, mappings, datasets, APIs, object-less actions, engine functions, flows, webhooks, connectors and email templates, supersedes nothing).
**Addenda**: [2026-08-08 — publish-time / marketplace namespace exclusivity](#addendum-2026-08-08-publish-time--marketplace-namespace-exclusivity) (**Accepted** 2026-08-08 — maintainer ruling on #1825, [acceptance comment 5226411687](https://github.com/objectstack-ai/objectstack/issues/1825#issuecomment-5226411687), landed by PR #6671; additive to §3.2, supersedes nothing); [2026-11-06 — bare names resolved without a package hold one package](#addendum-2026-11-06-bare-names-resolved-without-a-package-hold-one-package--34-narrowed) (**Accepted**, ruling C on #30798, phase 1; narrows §3.4 for picklists, mappings, datasets, APIs, object-less actions, engine functions, flows, webhooks, connectors, email templates, agents and skills — phase 1 complete — supersedes nothing).

---

Expand Down Expand Up @@ -267,28 +267,27 @@ package — stays covered by the `naming/namespace-prefix` lint in `os lint`.
> [Addendum (2026-10-08)](#addendum-2026-10-08-the-security-catalog-holds-one-name-per-deployment--34-narrowed).

> **Narrowed (2026-11-06) — picklists, mappings, datasets, APIs, object-less
> actions, engine functions, flows, webhooks, connectors and email templates are
> out of §3.4.** Their consumers resolve the name with no package — a field's
> actions, engine functions, flows, webhooks, connectors, email templates, agents
> and skills are out of §3.4.** Their consumers resolve the name with no package — a field's
> `picklist`, an import's `mappingName`, a dashboard's `datasetName`, the
> endpoint matcher's one API per name, `/actions/global/NAME`, a flow `script`
> node's function, the automation engine's one armed flow per name, the
> dispatcher's one declared webhook per name, the connector picker's one
> connector per name, a send's `template` — so with two holders every caller is
> connector per name, a send's `template`, `/meta/agent/NAME`, an MCP client's
> skill prompt — so with two holders every caller is
> served one of them, chosen by registration order, measured on a booted
> two-package deployment (#30803). Once the cold boot has sealed the registry, a
> package whose picklist, mapping, dataset, API, object-less action, flow,
> webhook, connector or email template bears a name another installed package
> holds is refused at managed install and at package registration (a built-in's
> webhook, connector, email template, agent or skill bears a name another
> installed package holds is refused at managed install and at package registration (a built-in's
> name, at managed install), and a package whose handlers bring a function
> another package holds is refused at the hook binder's bind, each naming both
> holders and the rename. ⛔ The environment catalog is never a holder: an
> environment item under a package's name is the ADR-0005 overlay. ⛔ Nor is the
> email service's built-in template bundle: an app's same-named template
> overrides it by contract (ADR-0131 D6), so that package holds no template name
> and is refused none. The remaining types the measurement found resolving
> without a package context — `agent` and `skill` — are to follow in the series'
> last change. Every other type, an action bound to an object among them, keeps
> §3.4 as written. See
> and is refused none. Every other type keeps §3.4 as written, among them a
> `tool` and an action bound to an object. See
> [Addendum (2026-11-06)](#addendum-2026-11-06-bare-names-resolved-without-a-package-hold-one-package--34-narrowed).

### 3.5 Namespace rename-on-install is deferred (non-goal)
Expand Down Expand Up @@ -956,7 +955,7 @@ first phase.

> **Status of this addendum: Accepted** — it records phase 1 of a maintainer
> ruling. It **narrows §3.4 for more metadata types, one family per change (P1.2,
> P1.7, P1.8), and supersedes no other text**: §§1–6, the 2026-08-08 addendum and
> P1.7, P1.8, P1.9 — the last), and supersedes no other text**: §§1–6, the 2026-08-08 addendum and
> the 2026-10-08 addendum stand verbatim.
>
> **Ruling this addendum records** — #30798, answered by the maintainer
Expand Down Expand Up @@ -1063,14 +1062,14 @@ the doors never refuse.
- **No downgrade.** `OS_METADATA_COLLISION=warn` softens the §3.2 namespace gate
only.

### P1.5 To follow
### P1.5 The series, complete

The same measurement found `agent` and `skill` resolving without a package
context too. They join P1.2's list in the series' last change under #30803, with
their own pins, narrowing §3.4 the same way (the maintainer kept all of them in
#30803 on 2026-11-06). `tool` and `rag_pipeline` share a reader with `agent` and
were not measured. The request family that was listed here is P1.7; the flow
family — `flow`, `webhook`, `connector` and `email_template` — is P1.8.
Phase 1 joined P1.2's list one family per change under #30803, each measured
first, as the maintainer ordered on 2026-11-06 (all of them kept in #30803): the
data family (P1.1), the request family (P1.7), the flow family (P1.8) and, last,
the AI family — `agent` and `skill` (P1.9). P1.9's change closes phase 1. `tool`
and `rag_pipeline` were never in the ruling's list; P1.9 records what was
measured of them.

### P1.6 Where it is implemented

Expand Down Expand Up @@ -1178,3 +1177,39 @@ and is not judged.
**The cold boot** reads the four with the registry's other holders, under
P1.3's switch: the flow precedence warning keeps naming both packages and arming
one, as before; the bare-name warning names every clash once with its rename.

### P1.9 The AI family: `agent`, `skill` — the series' last

Measured with P1.1's two packages, in both orders, plus a third package
installed after the boot (#30803, PR 4):

- **`agent`** — `/meta/agent/NAME` serves the FIRST-registered package's
agent, silently. It is the one by-name read of an agent the open framework
serves: the kernel `metadata` service, which the MCP `agent_prompt` reads,
answers no package agent at all on the composed runtime (ADR-0063 §2: agents
are platform-owned), and the agent runtime that resolves an agent by name for
chat is the cloud's, not measured here.
- **`skill`** — an MCP client's `prompts/get` of a skill resolves the
FIRST-registered package's skill (`listSkillPrompts` keeps the first row per
name and drops the other), silently; `/meta/skill/NAME` serves the same one.
ADR-0063 §2 names the skill the third-party extension primitive, so this is
the family's live collision.

A third package installed after the boot under either name installs (`200`) and
is shadowed. Neither type warns.

**What is refused.** Both join P1.2's list: the same two doors, the same
holders, the same envelope and rename. Neither is bundled, and neither has an
overridable baseline or a built-in name. An agent and a skill are two types: one
package's agent and another's skill under one name never meet.

**Not in the list.** A `tool` is not covered — the ruling's types do not name it.
Measured beside them: `/meta/tool/NAME` serves the first-registered package's
tool as well; the readers that resolve a tool by name are the cloud's. A
`rag_pipeline` cannot be declared through `defineStack` (the stack schema
refuses `ragPipelines`) and registered nothing in the measurement. Both are left
as §3.4 has them.

**The cold boot** reads the two with the registry's other holders, under P1.3's
switch (`'warn'`). This is the series' closing change: phase 1's list is
complete, and phase 2 (#30804, #30805) and phase 3 (#30806) follow.
28 changes: 25 additions & 3 deletions packages/objectql/src/bare-name-one-holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
* narrowed 2026-11-06). Each family of covered types joins in its own change
* under #30803: the data family first, then the request family (`api`, the
* object-less `action`, the engine's function registry), then the flow family
* (`flow`, `webhook`, `connector`, `email_template`).
* (`flow`, `webhook`, `connector`, `email_template`), and last the AI family
* (`agent`, `skill`), which closes the series.
*
* ## The rule
*
Expand Down Expand Up @@ -52,7 +53,14 @@
* per name, and a second one at boot already refuses the boot (ADR-0097);
* - `email_template` — a send's `template: NAME` resolves the
* FIRST-registered package's bundle, and `/meta/email_template/NAME`
* serves the same one.
* serves the same one;
* - `agent` — `/meta/agent/NAME` serves the FIRST-registered package's
* agent, the one by-name read of an agent the open framework serves (the
* agent runtime that resolves an agent by name is the cloud's);
* - `skill` — an MCP client's `prompts/get` of a skill keeps the
* FIRST-registered package's skill (`listSkillPrompts` keeps the first
* row per name and drops the other), and `/meta/skill/NAME` serves the
* same one.
*
* Every one of them flips with registration order, and none says so (the
* object-less action's handler bind warns that it replaces one, and names
Expand Down Expand Up @@ -145,7 +153,8 @@ import { isObjectLessActionKey, standaloneActionOwnerKey } from './action-govern
export type BareNameRegistryType =
| 'picklist' | 'mapping' | 'dataset'
| 'api' | 'action'
| 'flow' | 'webhook' | 'connector' | 'email_template';
| 'flow' | 'webhook' | 'connector' | 'email_template'
| 'agent' | 'skill';

/**
* A covered type: a registry type, or `function` — the engine's function
Expand All @@ -171,6 +180,9 @@ export const BARE_NAME_ONE_HOLDER_TYPES: readonly BareNameOneHolderType[] = Obje
'webhook',
'connector',
'email_template',
// the AI family
'agent',
'skill',
]);

/** The covered registry types, in the order a refusal lists them. */
Expand Down Expand Up @@ -213,6 +225,8 @@ const COLLECTION_KEY: Readonly<Record<BareNameRegistryType, string>> = Object.fr
webhook: 'webhooks',
connector: 'connectors',
email_template: 'emailTemplates',
agent: 'agents',
skill: 'skills',
});

/**
Expand All @@ -232,6 +246,8 @@ export const BUILT_IN_BARE_NAMES: Readonly<Record<BareNameOneHolderType, Readonl
webhook: new Set<string>(),
connector: new Set<string>(),
email_template: new Set<string>(),
agent: new Set<string>(),
skill: new Set<string>(),
});

/**
Expand Down Expand Up @@ -259,6 +275,8 @@ export const OVERRIDABLE_BASELINE_PACKAGES: Readonly<Record<BareNameOneHolderTyp
webhook: new Set<string>(),
connector: new Set<string>(),
email_template: new Set<string>(['com.objectstack.service.email']),
agent: new Set<string>(),
skill: new Set<string>(),
});

/**
Expand Down Expand Up @@ -383,6 +401,8 @@ const CONSUMER_CLAUSE: Readonly<Record<BareNameOneHolderType, string>> = Object.
webhook: 'the webhook dispatcher subscribes one declared webhook per name',
connector: 'a flow `connector_action` node and the connector picker name a connector',
email_template: 'a send names its email template',
agent: '`/meta/agent/NAME` serves one agent per name',
skill: 'an MCP client fetches a skill\'s prompt by its name',
});

/** The noun a reason names a covered type by. */
Expand All @@ -397,6 +417,8 @@ const TYPE_NOUN: Readonly<Record<BareNameOneHolderType, string>> = Object.freeze
webhook: 'a webhook',
connector: 'a connector',
email_template: 'an email template',
agent: 'an agent',
skill: 'a skill',
});

const joinList = (parts: readonly string[]): string =>
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {
import { SecurityCatalogNameConflictError, findEnvironmentHeldSecurityCatalogNames } from './registry.js';
// One name, one package holder for the bare-name types (a picklist, a mapping, a
// dataset, an API, an object-less action, a function, a flow, a webhook, a
// connector, an email template) — the cold boot's seal and its one switch
// connector, an email template, an agent, a skill) — the cold boot's seal and its one switch
// (`bare-name-one-holder.ts`, "The cold boot").
import { sealBareNameHoldersAtColdBoot } from './registry.js';
import { BARE_NAME_COLD_BOOT_MODE } from './bare-name-one-holder.js';
Expand Down
Loading
Loading