Skip to content

Commit b30325b

Browse files
committed
Merge origin/main into claude/issue-15429-decision-first-match
Landing lap 2: origin/main 40b315b, 6 commits past dcd3bce. #20350 (`40b315b0`) appended its connector-resilience sentence to step 18's `rationale`, the one conflict region (conversions/registry.ts and step 18's `conversionIds` merged cleanly). Resolved per the seat's answer B on #15429 (5865957805, re-applied by 5867190364), and nowhere else: main's text kept whole (the action-aria sentence, then the connector-resilience sentence), this branch's sentence appended verbatim; the one string join at the seam makes main's closing literal end in a space so the concatenation continues. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
2 parents 0c4dad2 + 40b315b commit b30325b

86 files changed

Lines changed: 4094 additions & 1165 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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/connector-mcp': patch
4+
'@objectstack/connector-openapi': patch
5+
'@objectstack/connector-rest': patch
6+
'@objectstack/connector-slack': patch
7+
'@objectstack/service-automation': patch
8+
---
9+
10+
feat(spec)!: retire the connector resilience family — `health` (health probe + circuit breaker), `status` and the nested `webhooks`, sixteen keys nothing read (#20273)
11+
12+
**BREAKING** — `connector.health` (the `healthCheck` probe, eight keys, and the
13+
`circuitBreaker`, six keys), `connector.status` and the connector-nested
14+
`webhooks` are removed from `ConnectorSchema` and `DeclarativeConnectorEntrySchema`
15+
— so from `defineConnector`, `stack.connectors[]`, the `PUT /api/v1/meta/connector/:name`
16+
door and `AutomationEngine.registerConnector`. ADR-0049 enforce-or-remove, one
17+
batch for the family, by the maintainer's criterion: does the mainstream platform
18+
offer this capability? Author-configured health probes and circuit breakers are
19+
not connector metadata in the mainstream (breakers live in API-gateway
20+
infrastructure), and an authored status and a nested webhook list duplicate what
21+
is already delivered here by other keys.
22+
23+
Measured before removal, each against a lit control: zero reads of any of the
24+
sixteen keys outside `packages/spec`. No loop ever polled a connector endpoint,
25+
counted consecutive failures or tripped a breaker, and none of the four
26+
`fallbackStrategy` behaviours existed. Nothing read an authored `status`: the
27+
runtime's dispatchability answer is the COMPUTED `state` (`ready` / `degraded`)
28+
on `GET /api/v1/automation/connectors`, which no authored value sets. A webhook
29+
nested in a connector was never registered as a `webhook` item, so it was never
30+
materialized into `sys_webhook` and never delivered.
31+
32+
### FROM → TO
33+
34+
| removed | what to write instead |
35+
| --- | --- |
36+
| `connector.health` (`healthCheck.*`, `circuitBreaker.*`, including `monitoringWindowMs` and the pre-rename `monitoringWindow`) | delete the block. Put health probes and circuit breaking in the connector provider or an upstream gateway. |
37+
| `connector.status` | delete the key. `enabled: false` on a declarative entry is what withdraws a materialized instance or marks a catalog-only descriptor; whether a registered connector can be dispatched is the computed `state`. |
38+
| `connector.webhooks` | delete the array. A webhook that is actually delivered is declared in the stack's top-level `webhooks:` collection — moving one there STARTS deliveries this connector never made, so decide per webhook. `events` and `signatureAlgorithm` have no counterpart there. |
39+
| `ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`, `ConnectorStatus`, `WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm` (schemas, types, `…Parsed` types) | no replacement — nothing parsed or constructed them. |
40+
41+
**The one-line fix: delete `health:`, `status:` and `webhooks:` from every connector.**
42+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
43+
44+
⚠️ Runtime behaviour is deliberately **unchanged**: none of the sixteen keys ever
45+
changed what a connector did. What changes is the answer an author gets — each
46+
key is refused at parse with a prescription, and in `tsc` (its input type is
47+
`never`), instead of being saved with no effect.
48+
49+
### The retirement kit
50+
51+
- **Tombstones.** `health`, `status` and `webhooks` are `retiredKey()` tombstones
52+
on the private `ConnectorBaseSchema` both published carriers wrap (the schema
53+
is not `.strict()`, so a bare deletion would be a silent strip, ADR-0104).
54+
`RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:{health,status,webhooks}`
55+
and `integration/DeclarativeConnectorEntry:{health,status,webhooks}`.
56+
- **Retired-default residue.** `status` was `.default('inactive')`, so every 17.x
57+
parse emitted `status: 'inactive'` into every connector; that exact value joins
58+
`connectionTimeoutMs: 30000` in the residue stage (accepted and stripped, so a
59+
def a 17.x toolchain built still registers). Every other value is refused.
60+
- **Seven defs leave whole** (`RETIRED_DEFS_BY_MAJOR[18]`): the four
61+
`integration/` schemas and three enums listed above.
62+
- **D2 conversion `connector-resilience-keys-removed`** (step 18, retired from
63+
the load path): strips the three keys from `connectors[]` and from stored
64+
`sys_metadata` connector rows (the rehydration seam replays it), one notice per
65+
key, as a lossless delete. Nested webhooks are stripped, never moved.
66+
- **The chain.** In the same step, `connector-health-and-trigger-durations-unit-in-key`
67+
renamed `health.circuitBreaker.monitoringWindow` to `monitoringWindowMs`. That
68+
breaker half is absorbed by this removal: the renamed key is itself removed, so
69+
an author holding either spelling ends with no `health` block. The
70+
conversion's `triggers[].interval` → `intervalSeconds` rename is unaffected.
71+
- **D3 entry `connector-resilience-keys-retired`** carries the family's
72+
judgement: which probe, breaker or nested webhook the author actually relied
73+
on, and where it goes now.
74+
- **Writers deleted.** The four shipped connector packages wrote
75+
`status: 'active'` and the automation service's degraded husk wrote
76+
`status: 'error'`; nothing read either back, and both writes are gone.
77+
- **No deprecation window**, per the project's startup-stage posture.
78+
79+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
80+
is published, so this is breaking for consumers no telemetry was consulted for.
81+
82+
Clause-②: no (narrowing)
83+
84+
<!-- adr-0087: registered connector-resilience-keys-removed, connector-resilience-keys-retired -->
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": minor
4+
---
5+
6+
A row-level-security predicate or a sharing-rule condition that compares two fields of different comparison classes — a text field with a number field, a field with a single image or file field, a field with a formula field — is refused when it is authored, at `os validate` / `os build` / `os lint` and, for a permission set, at the metadata save door (#20347). The classification it is judged by is exported once, from `@objectstack/spec/data`.
7+
8+
**BREAKING** — an accept-set narrowing in `@objectstack/lint`, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. `@objectstack/spec` gains exports only.
9+
10+
Clause-②: yes (narrowing)
11+
12+
`record.status != record.amount` (text vs number) and `record.status != record.photo` (text vs a single image) lower to a legal `{ status: { $ne: { $field: … } } }` filter and hold no list, so no authoring rule refused them. Measured before this change, through the real `os validate` and the real plugin-security and ObjectQL on driver-sql: `os validate` reported both valid; the read a `using` scopes answered `INVALID_FILTER` / 400 and a by-id update or delete it scopes `PERMISSION_DENIED` / 403, because driver-sql compiles a column-to-column comparison only between two columns of one comparison class; and a single-record insert judged by the `check` — or by a `using` standing in as the check — was admitted and stored, because the in-process write check compares the two raw values. A formula field (`record.status != record.is_open`) answered the same three ways. The same-class control (`record.status != record.note`) read, updated, deleted and inserted normally. For a sharing rule, the condition lowers and is seeded, and every criteria query it runs meets the same driver-sql refusal.
13+
14+
What changes:
15+
16+
- `@objectstack/spec/data` (`filter-cross-field-comparison-class.ts`): the cross-field comparison classification. `CROSS_FIELD_COMPARISON_CLASSES` names the six classes (`numeric`, `text`, `boolean`, `date`, `datetime`, `time`); `CROSS_FIELD_NO_CLASS_REASONS` the three families with none (`list-or-object`, `file`, `formula`); `CROSS_FIELD_COMPARISON_TYPE_CLASSES` classifies every `FieldType` member exactly once, by reference to the existing value-class sets; `crossFieldColumnVerdict` answers one declared column (a multi-capable type flagged `multiple: true` holds a list); and `crossFieldComparisonVerdict` answers two (`comparable`, `cross-class`, `no-class`, or `unjudged` for a type outside `FieldType`). It is lifted case for case from driver-sql's cross-field boundary, and a pairwise parity test in driver-sql holds the two equal over every declared field type.
17+
- `@objectstack/lint`: `validateRlsPredicateEnforceability` reports `rls-predicate-unenforceable`, and `validateSharingRuleEnforceability` reports `sharing-rule-unlowerable-condition`, for every lowered field-to-field comparison (`==`, `!=`, `>`, `>=`, `<`, `<=`, either side, under `!` too) whose two declared columns are not `comparable`. It judges `using` and `check` on every operation, and sharing-rule conditions. The finding names each comparison, each column's declared type and class, and the clause's run-time consequence; the hint lists every class with the declared types it holds, read from the spec. A comparison either side of which holds a list or an object stays the existing list-holding finding, and a clause either arm refuses is not also handed to the engine's filter judge, so one defect earns one finding.
18+
19+
Not changed: driver-sql and the in-process write check keep their own behaviour here; moving both onto the exported classification is the engine-lane half. A comparison between two columns of one class (`record.amount > record.budget`, `record.stage == record.account`), a file or formula field compared with a literal or tested against `null`, and any column the stack does not declare or declares with a type outside `FieldType`, are not reported.
20+
21+
No shipped predicate moves: of the 163 `using` / `check` / `condition` string literals in this repository's packages and examples, the 105 that lower hold two field-to-field comparisons, both same-class (`spent > budget`, a hook condition; `a > b`, a gate fixture), and neither is an RLS predicate or a sharing-rule condition.
22+
23+
To keep such a rule, compare a field only with a field of the same class, or with a literal or a `current_user` value; test a file field with `!= null`; or store the value the rule keys on in a field of the right type. If the two columns really hold comparable values, one of them is declared with the wrong type, and the declaration is what to fix.
24+
25+
<!-- adr-0087: not-required (no-migration-prescription) nothing is renamed, retired or respelled: no metadata key, export or operator changes shape and no stored metadata is rewritten, so `objectstack migrate meta` has nothing to do; the author's remedy is to change which two columns a predicate compares, which is a change to the policy they meant, not to a spelling. -->
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
Clause-②: no
7+
8+
Five live structured metadata keys are authorable in the metadata form: `object.access`, `object.highlightFields`, `object.requiredPermissions`, `object.searchableFields` and `permission.adminScope`. Each was **declared** by its schema, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY`, so an author's only door was the Source tab. Each now has exactly one row, whose control mirrors a row a registered form already carries for the same node shape:
9+
10+
- `highlightFields` and `searchableFields` (Basics, beside `nameField`) — `widget: 'string-tags'`, the view form's `searchableFields` row: a free-text chip list over `string[]`. A field picker is not offered because the object draft carries no source object for one to read its catalog from. A misspelt entry is not dropped quietly: publishing refuses it (`object-field-ref-unknown`, `searchable-field-unknown`, both at `error`), and so does `os validate`. The object schema's own parse does not judge these names.
11+
- `access` (Advanced, beside `sharingModel`) — a `composite` over one declared `default` select (`public` / `private`), the `lifecycle` row's shape. Absent still resolves to `public`.
12+
- `requiredPermissions` (Advanced) — `widget: 'json'`, **never** `string-tags`: the value is a union of `string[]` and a `{read, create, update, delete}` map, and the tag widget reads a non-array as an empty list and writes the list back, which would silently replace a stored per-operation map. With the `json` hint the renderer resolves the face from the stored value's branch, so a stored map is edited as a map.
13+
- `permission.adminScope` (System Permissions) — `widget: 'json'`, the hint every structured row on the permission form carries; the renderer derives a nested form over its six keys, and edits merge into the stored scope.
14+
15+
The help text states what the runtime does with each value, including what absence resolves to. The renderer behaviour described above is objectui's metadata-admin form engine at this repository's `.objectui-sha` pin.
16+
17+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks — hence the regenerated `platform-objects` metadata-form bundles, whose 12 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills.
18+
19+
⛔ **The gate that would notice a missing row is NOT landed here.** The reconciliation gate's top-level `zodOnly` direction stays unwired; this change lands offers only.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
**plugin-auth: under the `open` audience posture, the deployment can turn email verification off**
7+
8+
Clause-②: yes
9+
10+
Under `audience.posture: 'open'`, an explicit `emailAndPassword.requireEmailVerification: false`
11+
declared by the **deployment** is now honoured instead of refused at config entry. The deployment
12+
declares it through its stack config (the `AuthManager` constructor), host code calling
13+
`AuthManager.applyConfigPatch()`, or the `OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false` env override
14+
of the `auth.require_email_verification` setting. A sign-up is then signed in at once, with no
15+
verification mail. This is for a deployment with no mail transport that trusts its sign-ups,
16+
such as a pre-production environment, which otherwise dead-ends every new account at the verify
17+
page.
18+
19+
Nothing changes for anyone who does not opt out:
20+
21+
- `open` with the value absent or `true` still forces verification on.
22+
- `email_domain` still refuses an explicit `false`, from any source, with the same message. The
23+
domain allowlist is the only gate there, so an unverified sign-up could claim a colleague's
24+
address.
25+
- `invite_only` is unchanged.
26+
- A `false` stored only through the settings console is still refused under `open`. The console
27+
can agree with the deployment's opt-out, never make one.
28+
29+
The opt-out is loud. `AuthPlugin` logs one warning at boot naming the posture and the
30+
consequence: anyone can register an address they do not control, and an organization invitation
31+
sent to that address can then be accepted by that account. `getPublicConfig()` reports
32+
`requireEmailVerification: false`, the value actually wired, because the wiring and the
33+
advertisement now read one resolver.
34+
35+
`AuthManager.applyConfigPatch()` takes an optional second argument,
36+
`{ requireEmailVerificationFrom: 'deployment' | 'console' }`. It defaults to `deployment`; the
37+
settings binding passes `console` for a stored value and `deployment` for an env override.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ read at startup unless noted otherwise. Boolean variables accept `true` / `false
8181
| `OS_DISABLE_SIGNUP` | boolean | `false` | When `true`, block new email/password sign-ups. Under the `single` posture the very first user can still sign up to bootstrap admin; under the walled postures no sign-up is ever promoted, so this leaves the deployment dependent on `OS_PLATFORM_OWNER_EMAIL` alone. |
8282
| `OS_AUTH_EMAIL_PASSWORD_ENABLED` | boolean | settings default | Settings env override for `auth.email_password_enabled`. Controls local email/password login. |
8383
| `OS_AUTH_SIGNUP_ENABLED` | boolean | settings default | Settings env override for `auth.signup_enabled`. Takes precedence over UI settings and is preferred over `OS_DISABLE_SIGNUP`. |
84-
| `OS_AUTH_REQUIRE_EMAIL_VERIFICATION` | boolean | settings default | Settings env override for `auth.require_email_verification`. |
84+
| `OS_AUTH_REQUIRE_EMAIL_VERIFICATION` | boolean | settings default | Settings env override for `auth.require_email_verification`. Under the `open` audience posture, `false` is the deployment's opt-out from the forced verification (one boot warning; see [Email verification under each posture](/docs/deployment/self-hosting#email-verification-under-each-posture)); under `email_domain` it is refused, since verification is forced there. |
8585
| `OS_AUTH_MEMBERSHIP_POLICY` | `auto` \| `invite-only` | `auto` | Settings env override for `auth.membership_policy` — what a newly created user joins (ADR-0093 D1). `auto` binds every new user to the deployment's default organization. `invite-only` grants membership solely through an explicit act: creating a workspace, accepting an invitation, an admin adding them, or SSO just-in-time provisioning. Applies to sign-up **and** to the backfill of pre-existing member-less users. An unrecognized value is rejected with an `error` log and **ignored** — the deployment keeps its current policy rather than silently reverting to `auto`. |
8686
| `OS_AUTH_GOOGLE_ENABLED` | boolean | settings default | Settings env override for `auth.google_enabled`. Requires Google OAuth credentials from Settings or env. |
8787
| `GOOGLE_CLIENT_ID` | string | — | Deployment-level Google OAuth client id for the open-source Google login implementation. |

0 commit comments

Comments
 (0)