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
2 changes: 1 addition & 1 deletion docs/adr/0093-tenancy-mode-and-membership-lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0093: Tenancy mode as a first-class capability, and a single owner for the user→membership lifecycle

- **Status:** Proposed (implementation in progress)
- **Status:** Accepted (2026-07-13) — implemented: `tenancy` kernel service (`plugin-auth/src/tenancy-service.ts`), membership reconciler (`reconcile-membership.ts`), locked by `dogfood/test/membership-reconciler.dogfood.test.ts`
- **Date:** 2026-07-13
- **Deciders:** ObjectStack Protocol Architects
- **Implementation:** #2882 (Phase 0 — tactical create-user bind, merged) → this PR (Phases 1–3 — `tenancy` service, fail-fast boot guard, membership reconciler, consumer migration, backfill, docs). One revision from the original plan, ratified in D2: the endpoint-level create-user bind **delegates to the shared reconciler** (one implementation, two call sites) instead of being deleted. Runtime verification confirmed the hook fires for `admin.createUser`, but better-auth *defers* `user.create.after` post-commit (#1881), so the endpoint keeps its delegated call to report `organizationId` / `membershipCreated` deterministically in its response. Cloud-host semantics (personal-org hook precedence, multi-org non-binding, D5 blast radius) verified against `objectstack-ai/cloud` — see D2/D3/D5.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0095: Authorization Kernel Chain — Tenant Isolation as Layer 0, a Monotonic Posture Ladder, Capability-Derived Posture

**Status**: Proposed (2026-07-14)
**Status**: Accepted (2026-07-14) — implemented: D1 tenant Layer 0 (`plugin-security/src/tenant-layer.ts`), D2/D3 posture ladder (`core/src/security/posture-ladder.ts`), locked by `authz-matrix-gate.test.ts` + `posture-ladder.test.ts` (#2920 B1–B4)
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0002](./0002-environment-database-isolation.md) (environment-per-database), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (scope depth), [ADR-0066](./0066-unified-authorization-model.md) (unified authz model, superuser bypass), [ADR-0090](./0090-permission-model-v2-concept-convergence.md) (permission model v2), [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (tenancy service)
**Scopes**: the "cross-tenant RLS does not exist here" claim in [ADR-0073](./0073-automation-execution-identity.md) (see Context)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ADR-0096: Declarative Connector Instances — Provider-Bound `connectors:` Entries Materialized by Generic Executors
# ADR-0097: Declarative Connector Instances — Provider-Bound `connectors:` Entries Materialized by Generic Executors

**Status**: Accepted (2026-07-15) — implemented in framework#2977
**Deciders**: ObjectStack Protocol Architects
Expand Down
12 changes: 12 additions & 0 deletions docs/adr/PRIORITIZATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# ADR Prioritization Review

> ⚠️ **STALE (noted 2026-07-16):** this review predates ADR-0050 onward — the directory now
> reaches ADR-0097, so everything from 0050 up is outside its scope, and several statuses it
> records for 0001–0049 have since moved. Treat it as a historical snapshot, not current
> state. The `Status:` header of each ADR file (kept current per the hygiene rule below) is
> the authority; this document is retained for its method and its ranked-plan rationale.
>
> **Status-hygiene rule (adopted 2026-07-16):** when a PR lands the implementation of an ADR,
> the same PR (or an immediate follow-up) must flip that ADR's `Status:` line to
> `Accepted — implemented` with one line of evidence (key file + test). An ADR left
> `Proposed` after its code ships misleads every later reader — this is the same
> no-silently-stale discipline as ADR-0078, applied to the ADRs themselves.

> **Date:** 2026-06-12
> **Scope:** All 49 ADRs in `docs/adr/`, cross-checked against actual code on `main`.
> **Method:** Every unfinished ADR was read in full and its `Status:` header was
Expand Down
4 changes: 2 additions & 2 deletions examples/app-showcase/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default defineStack({
// • slack → registered so TaskCompletedSlackFlow resolves its connector;
// live posting needs a real bot token (set SLACK_BOT_TOKEN).
// • openapi → option-less: contributes only the `openapi` provider factory
// (ADR-0096), which materializes the StatusOpenApiConnector
// (ADR-0097), which materializes the StatusOpenApiConnector
// declarative instance below — its OpenAPI document is a
// package-relative FILE PATH read at boot (#3016).
plugins: [
Expand Down Expand Up @@ -197,7 +197,7 @@ export default defineStack({
// Declarative REST endpoints (object_operation + flow) — the metadata
// counterpart of the code-mounted recalc endpoint (see src/system/apis/).
apis: allApis,
// Declarative `connectors:` — both kinds (ADR-0096): provider-bound
// Declarative `connectors:` — both kinds (ADR-0097): provider-bound
// INSTANCES (StatusApiConnector via `rest`; StatusOpenApiConnector via
// `openapi` with a package-relative file-path spec, #3016) materialized into
// live, dispatchable connectors at boot, plus a CATALOG DESCRIPTOR
Expand Down
6 changes: 3 additions & 3 deletions examples/app-showcase/src/automation/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,22 +395,22 @@ export const TaskCompletedRestPingFlow = defineFlow({
});

/**
* Declarative Connector Ping — the worked ADR-0096 example: a `connector_action`
* Declarative Connector Ping — the worked ADR-0097 example: a `connector_action`
* dispatching a **provider-bound declarative connector instance**.
*
* Where {@link TaskCompletedRestPingFlow} targets the `rest` connector a *plugin*
* registered (ConnectorRestPlugin, ADR-0018 §Addendum), this flow targets
* `showcase_status_api` — a connector declared as pure metadata in
* src/system/connectors/ and *materialized* into the registry at boot by the
* `rest` generic executor (ADR-0096). Nothing registered it in code: the
* `rest` generic executor (ADR-0097). Nothing registered it in code: the
* `connectors:` entry named `provider: 'rest'`, and the automation service turned
* it into a live connector. On task creation the flow issues `GET /api/v1/health`
* through it; the call and its `{ status: 'ok' }` response are captured on the
* flow run, proving the declarative path dispatches end-to-end.
*/
export const ShowcaseDeclarativeConnectorPingFlow = defineFlow({
name: 'showcase_declarative_connector_ping',
label: 'Declarative Connector Ping (ADR-0096)',
label: 'Declarative Connector Ping (ADR-0097)',
description:
'Dispatches GET /api/v1/health through showcase_status_api — a provider-bound connector instance materialized from pure metadata at boot.',
type: 'autolaunched',
Expand Down
2 changes: 1 addition & 1 deletion examples/app-showcase/src/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const STACK_COLLECTION_COVERAGE: Record<string, KindCoverage> = {
status: 'demonstrated',
files: ['src/system/connectors/index.ts', 'src/automation/flows/index.ts'],
notes:
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCES (ADR-0096 / #2977): StatusApiConnector declares `provider: rest` (inline config) and StatusOpenApiConnector declares `provider: openapi` with its OpenAPI document referenced as a package-relative FILE PATH (#3016, read at boot and confined to the package root) — both are materialized into live, dispatchable connectors at boot; ShowcaseDeclarativeConnectorPingFlow calls the rest instance via connector_action and both appear in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCES (ADR-0097 / #2977): StatusApiConnector declares `provider: rest` (inline config) and StatusOpenApiConnector declares `provider: openapi` with its OpenAPI document referenced as a package-relative FILE PATH (#3016, read at boot and confined to the package root) — both are materialized into live, dispatchable connectors at boot; ShowcaseDeclarativeConnectorPingFlow calls the rest instance via connector_action and both appear in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
},
};

Expand Down
14 changes: 7 additions & 7 deletions examples/app-showcase/src/system/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { defineConnector, type Connector } from '@objectstack/spec/integration';

/**
* Declarative `connectors:` — the collection now holds BOTH kinds (ADR-0096):
* Declarative `connectors:` — the collection now holds BOTH kinds (ADR-0097):
*
* 1. **Provider-bound instance** ({@link StatusApiConnector}) — a live,
* dispatchable connector authored as pure metadata. It names a `provider`
Expand All @@ -14,7 +14,7 @@ import { defineConnector, type Connector } from '@objectstack/spec/integration';
* indistinguishable from a hand-written connector — `connector_action`
* dispatches it and `GET /connectors` lists it. {@link
* file://../../automation/flows/index.ts | ShowcaseDeclarativeConnectorPingFlow}
* calls it end-to-end. This is the #2977 / ADR-0096 upgrade of what used to
* calls it end-to-end. This is the #2977 / ADR-0097 upgrade of what used to
* be a purely descriptor-only collection.
*
* 2. **Catalog descriptor** ({@link ErpCatalogConnector}, the #2612 interim
Expand All @@ -31,7 +31,7 @@ import { defineConnector, type Connector } from '@objectstack/spec/integration';
*/

/**
* ADR-0096 provider-bound instance — declared as pure metadata, materialized
* ADR-0097 provider-bound instance — declared as pure metadata, materialized
* into a live `rest` connector at boot by ConnectorRestPlugin's provider factory
* (which the plugin registers even though, here, it is also configured with a
* hand-wired `rest` connector). Points at the running server itself, so
Expand All @@ -45,7 +45,7 @@ export const StatusApiConnector = defineConnector({
label: 'Status API (Declarative REST Instance)',
type: 'api',
description:
'Provider-bound declarative connector instance (ADR-0096): authored as metadata, materialized into a live, ' +
'Provider-bound declarative connector instance (ADR-0097): authored as metadata, materialized into a live, ' +
'dispatchable `rest` connector at boot. Unlike the ERP descriptor below, this one IS callable from a flow ' +
'connector_action and appears in GET /connectors.',
provider: 'rest',
Expand All @@ -59,7 +59,7 @@ export const StatusApiConnector = defineConnector({
auth: { type: 'none' },
});
/**
* ADR-0096 provider-bound instance, **file-path spec** form (#3016): the OpenAPI
* ADR-0097 provider-bound instance, **file-path spec** form (#3016): the OpenAPI
* document lives next to this file (`status-openapi.json`) and is referenced by
* a path resolved relative to THIS package's root at materialization — reads
* are confined to the package root (absolute / `..`-escaping paths are
Expand All @@ -76,7 +76,7 @@ export const StatusOpenApiConnector = defineConnector({
label: 'Status API (Declarative OpenAPI Instance, File-Path Spec)',
type: 'api',
description:
'Provider-bound declarative connector instance (ADR-0096) whose OpenAPI document is referenced as a ' +
'Provider-bound declarative connector instance (ADR-0097) whose OpenAPI document is referenced as a ' +
'package-relative file path (#3016) and read at boot, confined to the package root. Materialized into a live ' +
'`openapi` connector — getHealth dispatches GET /api/v1/health against the running server.',
provider: 'openapi',
Expand All @@ -102,7 +102,7 @@ export const ErpCatalogConnector = defineConnector({
authentication: { type: 'api-key', key: 'SET_AT_INSTALL_TIME', headerName: 'X-API-Key' },
// Descriptor-level action catalog: key + label + I/O JSON Schemas. Note the
// deliberate absence of any execution binding (HTTP method/path) — that is
// what keeps descriptors inert today and what ADR-0096's provider binding
// what keeps descriptors inert today and what ADR-0097's provider binding
// supplies declaratively.
actions: [
{
Expand Down
8 changes: 4 additions & 4 deletions packages/connectors/connector-mcp/src/connector-mcp-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createMcpProviderFactory, MCP_PROVIDER_KEY } from './mcp-provider.js';

/**
* Minimal surface of the automation engine this plugin depends on — the
* connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0096).
* connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
* Kept structural so the plugin needs no runtime dependency on
* `@objectstack/service-automation`.
*/
Expand All @@ -24,7 +24,7 @@ export interface ConnectorRegistrySurface {
}

/**
* Options for {@link ConnectorMcpPlugin}. All optional (ADR-0096): with no
* Options for {@link ConnectorMcpPlugin}. All optional (ADR-0097): with no
* `transport` the plugin contributes only the `mcp` provider factory — so a
* stack can declare `provider: 'mcp'` instances as pure metadata. Supply a
* `transport` to ALSO connect one hand-wired MCP server at `start()`.
Expand All @@ -34,7 +34,7 @@ export interface ConnectorMcpPluginOptions extends Partial<McpConnectorOptions>
/**
* ConnectorMcpPlugin — contributes the generic MCP adapter (ADR-0024) in two forms:
*
* 1. **Provider factory** (`mcp`, ADR-0096): registered at `init()` so the
* 1. **Provider factory** (`mcp`, ADR-0097): registered at `init()` so the
* automation service can materialize declarative `provider: 'mcp'`
* `connectors:` entries — connecting to the server and mapping its tools to
* connector actions — at boot.
Expand Down Expand Up @@ -65,7 +65,7 @@ export class ConnectorMcpPlugin implements Plugin {
}

async init(ctx: PluginContext): Promise<void> {
// Contribute the `mcp` provider factory (ADR-0096) before the automation
// Contribute the `mcp` provider factory (ADR-0097) before the automation
// service materializes declarative instances during its start().
const automation = this.tryGetAutomation(ctx);
if (automation && typeof automation.registerConnectorProvider === 'function') {
Expand Down
4 changes: 2 additions & 2 deletions packages/connectors/connector-mcp/src/mcp-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0096 — the `mcp` provider factory: materialize a declarative
// ADR-0097 — the `mcp` provider factory: materialize a declarative
// `provider: 'mcp'` connector instance by connecting to the server (an injected
// fake client here), listing its tools, and mapping them to actions.

Expand Down Expand Up @@ -33,7 +33,7 @@ function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProvider
return { name: 'github', label: 'GitHub', type: 'api', ...partial };
}

describe('mcp provider factory (ADR-0096)', () => {
describe('mcp provider factory (ADR-0097)', () => {
it('advertises the mcp provider key', () => {
expect(MCP_PROVIDER_KEY).toBe('mcp');
});
Expand Down
6 changes: 3 additions & 3 deletions packages/connectors/connector-mcp/src/mcp-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectsta
import { createMcpConnector, type McpConnectorOptions, type McpTransport } from './mcp-connector.js';

/**
* The provider key this package contributes (ADR-0096). A declarative
* The provider key this package contributes (ADR-0097). A declarative
* `connectors:` entry with `provider: 'mcp'` is materialized by this factory.
*/
export const MCP_PROVIDER_KEY = 'mcp';
Expand Down Expand Up @@ -96,15 +96,15 @@ function normalizeTransport(
}

/**
* Build the `mcp` {@link ConnectorProviderFactory} (ADR-0096 / ADR-0024). At boot
* Build the `mcp` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0024). At boot
* the automation service invokes it for each `provider: 'mcp'` declarative
* instance: it connects to the MCP server named by `providerConfig.transport`,
* lists its tools, and produces the same `{ def, handlers, close }` bundle
* {@link createMcpConnector} builds for a hand-wired MCP connector — one action
* per tool, dispatched to the server's `tools/call`.
*
* The connection is opened at materialization; an unreachable server or invalid
* transport therefore fails boot loudly (ADR-0096 fail-loud contract). Prefer a
* transport therefore fails boot loudly (ADR-0097 fail-loud contract). Prefer a
* fail-soft plugin instantiation for an *optional* server.
*/
export function createMcpProviderFactory(deps: McpProviderDeps = {}): ConnectorProviderFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-pr

/**
* Minimal surface of the automation engine this package depends on — the
* connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0096).
* connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
* Kept structural so callers need no runtime dependency on
* `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).
*/
Expand Down Expand Up @@ -35,7 +35,7 @@ export function registerOpenApiConnector(registry: ConnectorRegistrySurface, con
}

/**
* Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0096): with no
* Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no
* `document` the plugin contributes only the `openapi` provider factory — so a
* stack can declare `provider: 'openapi'` instances as pure metadata. Supply a
* `document` (+ config) to ALSO register one hand-wired OpenAPI connector.
Expand All @@ -49,7 +49,7 @@ export interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorC
* ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in
* two forms:
*
* 1. **Provider factory** (`openapi`, ADR-0096): registered at `init()` so the
* 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the
* automation service can materialize declarative `provider: 'openapi'`
* `connectors:` entries — loading the OpenAPI document and mapping each
* operation to a connector action — at boot.
Expand All @@ -74,7 +74,7 @@ export class ConnectorOpenApiPlugin implements Plugin {
}

async init(ctx: PluginContext): Promise<void> {
// Contribute the `openapi` provider factory (ADR-0096) before the
// Contribute the `openapi` provider factory (ADR-0097) before the
// automation service materializes declarative instances during its start().
const automation = this.tryGetAutomation(ctx);
if (automation && typeof automation.registerConnectorProvider === 'function') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0096 — the `openapi` provider factory: materialize a declarative
// ADR-0097 — the `openapi` provider factory: materialize a declarative
// `provider: 'openapi'` connector instance from a spec document + resolved auth.

import { describe, it, expect } from 'vitest';
Expand All @@ -22,7 +22,7 @@ function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProvider
return { name: 'pets', label: 'Pets', type: 'api', ...partial };
}

describe('openapi provider factory (ADR-0096)', () => {
describe('openapi provider factory (ADR-0097)', () => {
it('advertises the openapi provider key', () => {
expect(OPENAPI_PROVIDER_KEY).toBe('openapi');
});
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('openapi provider factory (ADR-0096)', () => {
await expect(factory(ctx({ providerConfig: {} }))).rejects.toThrow(/providerConfig\.spec/);
});

// ── File-path specs (#3016 — ADR-0096 follow-up) ────────────────────────
// ── File-path specs (#3016 — ADR-0097 follow-up) ────────────────────────

it('reads a file-path spec through the host loadPackageFile capability', async () => {
const requested: string[] = [];
Expand Down
Loading
Loading