Skip to content

Commit 1421dd9

Browse files
committed
merge main (#28096 landed) into the projection branch
Claude-Session: https://claude.ai/code/session_01Utp6CYjLJ7sxfUehRrLbT3 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 88b8a2b + 73b7270 commit 1421dd9

49 files changed

Lines changed: 1788 additions & 159 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
'@objectstack/cli': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(service-datasource): a datasource defined in code reads its credential at connect from `OS_DATASOURCE_<NAME>_PASSWORD`
8+
9+
Clause-②: yes (widening) — `@objectstack/service-datasource` exports `datasourceEnvCredentialVar`, `resolveDatasourceEnvCredentialsSwitch`, `DATASOURCE_ENV_CREDENTIALS_SWITCH` and the `DatasourceEnvCredentialSource` type, and `DatasourceConnectionServiceConfig` gains the optional `envCredentials`; `@objectstack/spec/data` exports `CODE_DEFINED_CREDENTIAL_ROUTE`. Nothing an author writes is added or refused.
10+
Metadata-protocol: none
11+
12+
A datasource declared in code (`defineDatasource`, or a package's `*.datasource.ts`) had no way to get its password: the driver contracts refuse `config.password` and a `user:password@` URL, the Setup → Datasources form cannot edit a code datasource, and nothing mints a `sys_secret:` handle for one. Now the credential stays out of the declaration and is read at connect from a variable derived from the datasource name: `warehouse` reads `OS_DATASOURCE_WAREHOUSE_PASSWORD` (upper-cased; every character outside `A-Z0-9` becomes `_`). The value is the connection's secret: the password for postgres, mysql and mongodb, the `authToken` for turso.
13+
14+
- **A host switch.** `OS_DATASOURCE_ENV_CREDENTIALS` takes `on` or `off`; any other value refuses the boot with a sentence. `os serve` (and so `os start` and `os dev`) turns it on unless the operator set it. Every other composition leaves it off, so a host that loads several tenants' artifacts into one process never reads the variable.
15+
- **Code datasources only, by provenance.** The variable is read on the boot's declared connect, for a name in the host's code-datasource set: the booted artifact's datasources and those of a package installed locally. A datasource created through Setup or the API under the same name never reads it, and neither does the `default` datasource, which keeps `OS_DATABASE_URL`. A record's own `origin` is never consulted.
16+
- **A declared credential wins.** With `external.credentialsRef` or `external.connector` declared the variable is not read, and a reference that fails to resolve stays fail-closed.
17+
- **Unset.** The connect goes ahead without a secret as before; when the database then refuses, the reason names the variable to set.
18+
- **Two names deriving one variable** are refused at their connect.
19+
- **Who can read it.** Any code datasource the host registers under a name reads that name's variable, including one in an installed package. Install only packages you trust with the credentials in the process environment.
20+
21+
The credential refusals (`config.password`, a URL userinfo password, a credential query parameter, the mongo `options` passthrough) and the two `password` guidance entries on `DatasourceSchema` / `ExternalDatasourceSettingsSchema` end with one shared sentence naming this route, after the Setup and `external.credentialsRef` routes they already name. `planCredentialMigration`'s remedy for a code-defined row names the row's variable.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
feat(platform-objects): the platform identity actions an agent may reach declare it — additive acts exposed, destructive lifecycle acts exposed behind confirmation, credential and ownership acts human-only
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `sys_*` identity objects' actions now carry the ADR-0011 `ai` block by risk, as ADR-0011's 2026-10-30 addendum records (58 actions assessed, 38 declared):
11+
12+
- **Exposed (21).** Additive or self-scoped acts declare `ai: { exposed: true, description }`: the four `invite_user` mirrors, resending and accepting an invitation, adding a member to an organization or a team, creating, editing and renaming a team or an organization, switching the active organization, re-enabling an OAuth application, enabling two-factor authentication, updating your own profile, resending the verification email, and requesting SSO domain verification.
13+
- **Exposed, confirmation required (17).** Destructive or privilege-changing lifecycle acts also declare `requiresConfirmation: true`, so MCP `run_action` refuses them `428 ACTION_CONFIRMATION_REQUIRED`, with nothing run, until the call carries `confirm: true`: banning, unbanning and unlocking a user, setting a user's manager, revoking an API key or a session, signing out other devices, cancelling or declining an invitation, removing a member or a team member, deleting a team, an OAuth application or an SSO provider, disabling an OAuth application, leaving an organization, and unlinking a sign-in provider.
14+
- **Human-only (20).** Acts that mint a credential or a session, change ownership, a role or a sign-in factor, or destroy an account or an organization (impersonating a user, setting a password, creating a user or an API key, transferring ownership, changing a member's role, deleting an organization, and the rest the addendum names) declare no `ai` block and are not listed over MCP. `sys_invitation.copy_invitation_link` declares none either; it was not part of the assessment.
15+
16+
Over MCP these actions are listed and run behind every existing gate: the capability gate, the installation's action switch, the param contract, the confirmation gate and the identity route's own authorization. They are `type: 'api'` actions forwarded to identity routes that accept a signed-in session only, so an agent connected over OAuth sees them listed and is refused at the route when it runs one.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the field-types guide names the backfill for an amount switched to record mode; the dashboards guide says a record-mode amount aggregate is served only grouped by its code field
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **`objectstack-data/rules/field-types.md`**: an amount field with stored rows that is switched to `currencyMode: 'record'` is backfilled with `os migrate record-currency-codes`. The derived-measure sentence moves out of this guide; it already sits in the dashboards guide, which the line now points to.
11+
- **`objectstack-ui/rules/dashboards.md`**: a record-mode amount refuses `sum` / `avg` / `min` / `max` unless grouped by its code field. The old "ungrouped" read as if any grouping would do.

‎content/docs/ai/connect-mcp.mdx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,20 @@ can reach is the **default exposure policy (v1)**:
261261
The stored-metadata tables (`sys_metadata`, `sys_metadata_history`) stay
262262
refused even when flagged. Each action `list_actions` returns carries
263263
`aiExposed: true`, the server's attestation of that opt-in.
264+
- **The platform's own identity actions declare it by risk.** Additive acts
265+
(inviting someone, adding a member to a team, creating or renaming a team or
266+
an organization, re-enabling an OAuth application) are exposed. Destructive
267+
lifecycle acts (banning a user, revoking a session or an API key, removing a
268+
member, deleting a team, leaving an organization) are exposed with
269+
`ai.requiresConfirmation: true`, so `run_action` refuses them until the call
270+
carries `confirm: true`. Acts that mint a credential or a session, change
271+
ownership or a sign-in factor, or destroy an account or an organization
272+
(impersonating a user, setting a password, creating an API key, transferring
273+
ownership, deleting an organization) declare nothing and are not listed. The
274+
full list is ADR-0011's 2026-10-30 addendum. These actions forward to
275+
identity routes that accept a signed-in session only, so an agent connected
276+
over OAuth sees them listed, but a call that reaches the identity route is
277+
refused there (`401`) and changes nothing.
264278

265279
<Callout type="info">
266280
This is the default policy, not a ceiling. A finer, **metadata-declared** exposure

‎content/docs/automation/connectors.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ retry):
579579
| MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic |
580580
| A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer) | **Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it |
581581
| Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** |
582+
| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) | **The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input against the `inputSchema` yet |
582583
| A bad entry arrives via **runtime reload** (a Studio publish, a direct active save through `PUT /api/v1/meta/connector/:name`, or a delete, made on any replica — peers reconcile on [cluster lane 2](/docs/kernel/cluster#62-the-cluster-invalidation-family); or a dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes |
583584

584585
## Catalog descriptors: entries without a `provider`

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -944,11 +944,13 @@ export default defineStack({
944944
```
945945

946946
Each `url` must be credential-free when the stack is published: a
947-
`user:password@` URL is refused, like an inline `config.password`. Bind the
948-
password through `external.credentialsRef` instead, keeping the user in the URL
949-
(`mongodb://svc@host/db`): the bound secret is injected beside it at connect. The
950-
[read-only example](#read-only-grant-it-at-the-database-not-in-metadata) below
951-
shows the binding.
947+
`user:password@` URL is refused, like an inline `config.password`. Keep the user
948+
in the URL (`mongodb://svc@host/db`) and supply the password at runtime: a
949+
datasource declared in code reads it at connect from `OS_DATASOURCE_<NAME>_PASSWORD`
950+
(`OS_DATASOURCE_ANALYTICS_PASSWORD` here), which `os serve` / `os start` honour by
951+
default, and the value is injected beside the user. A datasource created through
952+
Setup binds it through `external.credentialsRef` instead. See
953+
[Credentials](/docs/data-modeling/external-datasources#4-credentials).
952954

953955
Then in your object definition:
954956

@@ -990,19 +992,21 @@ export const Analytics = defineDatasource({
990992
driver: 'postgres',
991993
// The connection itself cannot write. Nothing in the app can talk past it.
992994
config: { host: 'db.internal', database: 'analytics', username: 'analytics_ro' },
993-
// The account's password, bound through the secret store (see below).
994-
external: { credentialsRef: 'sys_secret:9f2c…' },
995+
// The account's password is read at connect from OS_DATASOURCE_ANALYTICS_PASSWORD.
995996
active: true,
996997
});
997998
```
998999

9991000
The read-only account's password never goes in `config`: the driver contract
1000-
refuses `config.password`, and a `user:password@` URL, at publish. Bind it
1001-
instead. The Setup → Datasources connection form's secret field encrypts it into
1002-
`sys_secret` and stores the opaque handle at `external.credentialsRef`, or you
1003-
write a handle the secret store minted there yourself. `credentialsRef` is the
1004-
one `external` key a `managed` datasource may carry; every federation key is
1005-
still refused there. See
1001+
refuses `config.password`, and a `user:password@` URL, at publish. Because this
1002+
datasource is defined in code, the runtime reads the password at connect from
1003+
`OS_DATASOURCE_ANALYTICS_PASSWORD`, derived from its name. The CLI host
1004+
(`os serve` / `os start`) reads it by default; elsewhere the host turns it on
1005+
with `OS_DATASOURCE_ENV_CREDENTIALS=on`. The same datasource created through
1006+
Setup → Datasources binds the password instead: the connection form's secret
1007+
field encrypts it into `sys_secret` and stores the opaque handle at
1008+
`external.credentialsRef`. `credentialsRef` is the one `external` key a `managed`
1009+
datasource may carry; every federation key is still refused there. See
10061010
[Credentials](/docs/data-modeling/external-datasources#4-credentials).
10071011

10081012
Note that a read-only account also refuses **DDL**, so such a datasource cannot

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ const policy: DatasourceConnectPolicy = {
258258
Never inline a password. Put a reference in `external.credentialsRef` and store the
259259
secret in the secret store (the same `SecretBinder` / `ICryptoProvider` the
260260
runtime-admin "Add Datasource" wizard uses). The credential is resolved to
261-
cleartext **at connect, before the driver is built**.
261+
cleartext **at connect, before the driver is built**. A datasource defined in code
262+
reads an environment variable instead; see
263+
[A datasource defined in code](#a-datasource-defined-in-code-the-environment-variable).
262264

263265
The shipped binder encrypts the cleartext into a `sys_secret` row and mints an
264266
**opaque** handle — `sys_secret:<id>` — as the `credentialsRef`; that is the only
@@ -274,6 +276,77 @@ for the datasources listed under [When auto-connect fails](#when-auto-connect-fa
274276
and degrades with a warning otherwise. (Credential-less drivers such as SQLite
275277
simply have no `credentialsRef`.)
276278

279+
### A datasource defined in code: the environment variable
280+
281+
The handle above is minted by the Setup → Datasources form, which cannot edit a
282+
datasource defined in code (`defineDatasource`, or a `*.datasource.ts` file in a
283+
package). A code author leaves the credential out of the declaration entirely,
284+
and the runtime reads it **at connect** from an environment variable whose name
285+
the platform derives from the datasource name:
286+
287+
| Datasource name | Variable read at connect |
288+
| :--- | :--- |
289+
| `warehouse` | `OS_DATASOURCE_WAREHOUSE_PASSWORD` |
290+
| `crm_replica` | `OS_DATASOURCE_CRM_REPLICA_PASSWORD` |
291+
292+
The name is upper-cased and every character outside `A-Z0-9` becomes `_`. The
293+
value is handed to the driver as the connection's secret: the password for
294+
`postgres`, `mysql` and `mongodb`, the `authToken` for `turso`. It is never
295+
logged, stored or answered.
296+
297+
```typescript
298+
import { defineDatasource } from '@objectstack/spec/data';
299+
300+
export const Warehouse = defineDatasource({
301+
name: 'warehouse',
302+
driver: 'postgres',
303+
// No password here: it is read from OS_DATASOURCE_WAREHOUSE_PASSWORD at connect.
304+
config: { host: 'db.internal', database: 'warehouse', username: 'svc_app' },
305+
});
306+
```
307+
308+
```bash
309+
OS_DATASOURCE_WAREHOUSE_PASSWORD='…' os start
310+
```
311+
312+
What the slot does and does not do:
313+
314+
- **It is a host switch, `OS_DATASOURCE_ENV_CREDENTIALS`, which takes `on` or
315+
`off`.** `os serve`, `os start` and `os dev` turn it on unless you set it
316+
yourself. Every other composition (a host that builds its own kernel, a cloud
317+
runtime) leaves it off, so the variable is never read there. Any other value
318+
is refused at boot.
319+
- **It only serves datasources the host registers from code.** That means the
320+
ones in the booted artifact and in packages installed locally. A datasource
321+
created through Setup or the API under the same name never reads the
322+
variable. Nor does the `default` datasource, which keeps `OS_DATABASE_URL`.
323+
- **A declared credential wins.** With `external.credentialsRef` or
324+
`external.connector` declared, the variable is not consulted. If that
325+
reference fails to resolve, the datasource stays unconnected (fail-closed, as
326+
above) and does not fall back to the environment.
327+
- **An unset variable leaves the connect as it was.** The driver connects
328+
without a secret; if the database refuses, the datasource is unconnected and
329+
its reason names the variable to set.
330+
- **Two datasources that derive one variable are refused.** Valid datasource
331+
names are lower-case, so this cannot happen between two declarations that
332+
pass the schema; the guard covers a host that hands `connect()` other names.
333+
- **A plugin driver that resolves its credential per request** (the OData
334+
driver resolves `credentialsRef` that way) does not read this slot: it feeds
335+
only the secret a driver receives when its connection is built.
336+
337+
<Callout type="warn">
338+
**Who can read the variable.** The derived name is the only key, so any code
339+
datasource the host registers under that name reads it. Under `os serve` that
340+
includes every package installed into the environment: **a package can declare
341+
a datasource of any free name and so read the matching
342+
`OS_DATASOURCE_<NAME>_PASSWORD`**. Install only packages you trust with the
343+
credentials in that process's environment. On a host where several tenants'
344+
artifacts share one process, a process-wide variable would be readable by every
345+
tenant artifact that declares the name. That is why the switch is off outside the
346+
CLI host; turn it on there only when one operator owns every artifact the process
347+
loads.
348+
</Callout>
349+
277350
### Borrowing a connector instance's credential
278351

279352
An **API-backed** datasource, one served by a plugin driver such as the

‎content/docs/references/data/driver-mongo.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ MongoDB Connection Configuration
4747
| **host** | `string` | optional (default: `"localhost"`) | Host address |
4848
| **port** | `integer` | optional (default: `27017`) | Port number |
4949
| **username** | `string` | optional | Authentication user |
50-
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` |
50+
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config`. A datasource defined in code (`defineDatasource`) leaves the credential out of the declaration: it is read at connect time from the runtime environment variable derived from the datasource name (`OS_DATASOURCE_WAREHOUSE_PASSWORD` for a datasource named `warehouse`), which `os serve` / `os start` honour by default (`OS_DATASOURCE_ENV_CREDENTIALS=on`). |
5151
| **authSource** | `string` | optional | Authentication database |
5252
| **options** | `Record<string, any>` | optional | Extra MongoClient options (replicaSet, tls, timeouts, …). Credential-spelled keys (`password`, `authToken`, and their former aliases) are refused inline at any depth — bind the secret via the connection form / external.credentialsRef. `proxyPassword`, `tlsCertificateKeyFilePassword`, `key`, and `passphrase` are accepted and stored at rest in cleartext; they're redacted only when the datasource is read back, not refused at write. |
5353

‎content/docs/references/data/driver-mysql.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ MySQL / MariaDB connection configuration
4848
| **port** | `integer` | optional (default: `3306`) | Port number |
4949
| **database** | `string` | optional | Database name |
5050
| **username** | `string` | optional | Authentication user |
51-
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` |
51+
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config`. A datasource defined in code (`defineDatasource`) leaves the credential out of the declaration: it is read at connect time from the runtime environment variable derived from the datasource name (`OS_DATASOURCE_WAREHOUSE_PASSWORD` for a datasource named `warehouse`), which `os serve` / `os start` honour by default (`OS_DATASOURCE_ENV_CREDENTIALS=on`). |
5252
| **ssl** | `boolean` | optional | Enable TLS. Certificates go in the datasource-level `ssl` block. |
5353
| **autoMigrate** | `Enum<'off' \| 'safe'>` | optional | Dev-only non-destructive schema self-heal |
5454

0 commit comments

Comments
 (0)