Skip to content

Commit 76f6db1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4250-test-core-stall
# Conflicts: # package.json
2 parents 606d8af + 02dc076 commit 76f6db1

191 files changed

Lines changed: 16591 additions & 941 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
---
4+
5+
fix(approvals): an approval decision can no longer strand a flow run silently when no automation engine is attached (#4420)
6+
7+
#4420's fix closed every path by which a decision could be recorded while its
8+
flow stayed parked — except one, and it is the one where none of the new guards
9+
could run. Every guard it added (`assertRunResumable`'s pre-flight, the
10+
`RESUME_TARGET_LOST` refusal, the `RESUME_FAILED` throw) hangs off the
11+
automation engine. In a process where **no engine is attached**, all of them
12+
were skipped by the same `typeof this.automation?.resume === 'function'`
13+
condition that wrapped the resume itself — so the decision was written, the
14+
mirrored status field advanced, and the call answered HTTP 200 with
15+
`resumed: false` and **nothing logged at all**. That is #4420's reported
16+
symptom exactly, reproduced in the one composition its fix could not see.
17+
18+
The composition is reachable the same way the original bug was: a flow parks at
19+
an `approval` node in a process that has the automation service, and the
20+
decision arrives in one that does not (the plugin failed to init, or the host
21+
was recomposed between releases). The request row still carries a
22+
`flow_run_id` — which is the row's own declaration that a run is parked on this
23+
decision.
24+
25+
**What changes.** The decision still stands. Rolling it back is not on the
26+
table (a human really decided, and the row is durable by then), and refusing
27+
every such call would break the standalone approvals compositions the
28+
pre-flight deliberately protects — so `finalized` and `resumed` are unchanged
29+
for every existing caller. What changes is that the gap is no longer silent:
30+
31+
- it is logged at **`error`**, per the durability rule in `AGENTS.md`
32+
persisted state and runtime state disagree while nothing looks broken from
33+
the outside, which is the class that rule exists for;
34+
- the response carries **`resumeError`**, so `resumed: false` arrives with its
35+
reason and the stranded run's id instead of leaving the caller to guess
36+
whether a resume was even attempted.
37+
38+
It reuses the already-registered `RESUME_FAILED` code and the existing resume
39+
message shape rather than introducing a new vocabulary — the fact being
40+
reported (an outcome recorded whose run did not advance) is the same one.
41+
42+
Applied at all five sites that resume a recorded outcome: `decide`, the
43+
revision-limit auto-rejection, `sendBack`, `resubmit`, and both branches of
44+
`recall` (whose revise-window path needs `cancelRun` rather than `resume`).
45+
46+
A request that names **no** run is unaffected and stays quiet — there is
47+
nothing parked on it, and reporting one there would be the mirror-image
48+
failure that trains operators to skim `error`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
---
3+
4+
docs(plugin-auth): the two admin-identity audit comments no longer claim better-auth's writes bypass the ObjectQL lifecycle hooks plugin-audit subscribes to (#4940). `writeAdminAudit` in `admin-user-endpoints.ts` and the run-level insert in `admin-import-users.ts` both justified their explicit `sys_audit_log` row with the same stale mechanism claim #4802 refuted one layer up: *better-auth writes bypass the ObjectQL hooks that plugin-audit subscribes to, so admin identity operations would otherwise leave no compliance trail*.
5+
6+
Re-verified, then **measured** rather than reasoned about: the adapter writes with a plain `dataEngine.insert(...)`, `ObjectQL.insert()` fires `triggerHooks('afterInsert')` inside `executeWithMiddleware()`'s executor, and plugin-audit registers `writeAudit` as a bare `engine.registerHook('afterInsert', …)` with no object filter. Driving the real routes against the showcase stack with `AuditPlugin` installed: `POST /admin/create-user` leaves **two** `action: 'create'` rows on the new `sys_user` (the explicit one and plugin-audit's row snapshot) plus an `update` row for the must-change-password stamp; each imported user likewise gets plugin-audit's own per-row `create` row.
7+
8+
The explicit rows are **kept** — the discipline was right, only the reason was wrong — and the reasons are now the ones that hold. plugin-audit is an **optional** plugin, so without it there is no `sys_audit_log` table at all. `sys_account` **is** in plugin-audit's `SKIP_OBJECTS`, so the credential write behind `/admin/set-user-password` produces **zero** generic rows: that explicit row is the only trail an administrative password reset leaves, and deleting it on the strength of "the hook covers it" would silently destroy it. And the import's run-level row (`action: 'import'`, `record_id: null`) is a shape plugin-audit's `actionFor` cannot emit, answering what no per-row ledger can — who ran which import, and what it did overall.
9+
10+
The stale sentences are **refuted in place rather than deleted**, matching #4802's handling: the claim was copied into cloud's agent-facing docs (cloud#1022), and a reader arriving from a copy needs to see it named and corrected instead of finding silence.
11+
12+
Every measured fact is pinned by a new gate — `packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts` — so the corrected comment is an assertion the suite enforces rather than a fresh unverified claim. Comments and tests only; no runtime behaviour changes, nothing released.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
---
3+
4+
docs(plugin-auth): the `databaseHooks` doc comment no longer claims better-auth's adapter bypasses the ObjectQL middleware chain (#4802). The option's JSDoc in `auth-manager.ts` (and its two sibling copies — the wiring comment beside `composeDatabaseHooks`, and `AuthPluginOptions.databaseHooks` in `auth-plugin.ts`) justified "use `databaseHooks`, not an ObjectQL middleware" with a mechanism claim that no longer holds: *better-auth's adapter goes through `dataEngine` directly, bypassing the `ql.registerMiddleware` chain*.
5+
6+
Re-verified hop by hop against `main`: `ObjectQLPlugin` registers **one** engine instance under both service names (`registerService('objectql', this.ql)` and `registerService('data', this.ql)`, and nothing else in the repo registers `data`); `AuthPlugin` passes exactly that instance to `createObjectQLAdapterFactory`; the adapter writes with a plain `dataEngine.insert(objectName, …)` — there is no bypass or skip-middleware option to pass; and `ObjectQL.insert()` wraps its body in `executeWithMiddleware()`, whose only filter is the object name. So `ql.registerMiddleware(fn, { object: 'sys_user' })` **does** fire for better-auth's writes, and so do the engine's `beforeInsert`/`afterInsert` hooks — the SCIM identity-source stamp in `auth-plugin.ts` is built on precisely that.
7+
8+
The **rule is unchanged** — user-lifecycle invariants still belong in `user.create.after`, not in a `sys_user` middleware — but the reason is now the one that is actually true: **ADR-0093 D2**, one owner for the invariant on the one seam every creation path already flows through (self-signup, admin create-user, import, SSO JIT). The narrower fact that survives is written down instead of the false one: adapter writes carry `context.isSystem: true` (`withSystemContext`, pinned by `objectql-adapter.test.ts`), so every *authorization* middleware — security, sharing, the ADR-0092 identity write guard — early-returns by design; a middleware that gates on `isSystem` sees nothing, one that does not, runs.
9+
10+
The stale sentence is **refuted in place rather than deleted**, because it had been copied into cloud's agent-facing docs and had already killed the middleware option in two rounds of design work there (cloud#1012, handed over as cloud#1022). A reader arriving from one of those copies needs to see the claim named and corrected; a silent deletion would leave them assuming the framework comment is the stale one.
11+
12+
Comments only — no runtime behaviour changes, nothing released.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
---
3+
4+
ci(deps): lift the `brace-expansion` pin to 5.0.9 so `Validate Package Dependencies` stops failing on every PR (#4945)
5+
6+
`GHSA-rgw5-rvv9-x895` (7.5 high) affects `brace-expansion` 5.0.8 — which is
7+
exactly the version the previous pin (`brace-expansion@>=5.0.0 <5.0.8: ^5.0.8`,
8+
added for `GHSA-mh99-v99m-4gvg`) had settled on. The OSV-Scanner step in
9+
`.github/workflows/validate-deps.yml` reads `pnpm-lock.yaml` directly and exits
10+
non-zero on any match, so the job was red on `main` itself and attached that red
11+
to every PR that touched a manifest or the lockfile, whatever the PR contained
12+
(observed on #4944, which never touched `pnpm-lock.yaml`).
13+
14+
The `pnpm-workspace.yaml` override bound moves to `<5.0.9` / `^5.0.9`. It stays a
15+
transitive-only pin — nothing declares `brace-expansion` directly; it arrives via
16+
`minimatch` (ts-morph, eslint, `@typescript-eslint`, glob, `@vscode/vsce`,
17+
archiver), so no published manifest changes and `check-override-consistency`
18+
still has nothing to reconcile. 5.0.8 disappears from the lockfile entirely; the
19+
three `minimatch` snapshots that referenced it now resolve 5.0.9.
20+
21+
The reason to fix this on its own rather than let it ride along with the next
22+
dependency PR is the one the issue names: a permanently red required check
23+
trains everyone to scroll past it, and the next real advisory will look exactly
24+
like this one in the PR list.
25+
26+
Lockfile and override metadata only; releases nothing.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): stop silently stripping widget config off a bulk-action param option (#4001)
6+
7+
`BulkActionParamSchema`'s `options[]` entry is now `.passthrough()`, matching its
8+
parent. **Behaviour change, loosening only**: keys other than `label` / `value` on
9+
an option used to be *removed at parse* and are now preserved. Nothing that parsed
10+
before stops parsing, and no key changes meaning — an authored option simply keeps
11+
what it was written with.
12+
13+
Concretely, this used to happen without a warning:
14+
15+
```ts
16+
// authored
17+
options: [{ label: 'In Review', value: 'in_review', color: '#8B5CF6', icon: 'eye' }]
18+
// parsed, BEFORE
19+
options: [{ label: 'In Review', value: 'in_review' }]
20+
```
21+
22+
`color` and `icon` are not decoration the renderer ignores. objectui's
23+
`bulkParamToField` spreads every option entry into the field metadata
24+
(`packages/plugin-grid/src/components/bulkParamToField.ts:131`), where the widget
25+
vocabulary is `SelectOptionMetadata` (`packages/types/src/field-types.ts:288`) —
26+
`color`, `icon`, `disabled` and `visibleWhen` beyond the declared pair, and read
27+
(`option?.color`, `packages/fields/src/index.tsx:1089`). So the strip deleted
28+
config that would otherwise have rendered, on the authoring side, invisibly.
29+
30+
Nothing to migrate. If you dropped option colors/icons because "the spec ate them",
31+
they work now. The strictness ledger's prose already described this level as
32+
deliberately open while only the parent schema said so in code; the code now says
33+
it too, which is the part a machine can check.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): the boot banner's `Tenancy:` row now reports the resolved posture, not the superseded boolean (#4801)
6+
7+
`printServerReady` printed `Tenancy: multi-tenant | single-tenant` from a boolean
8+
`multiTenant` that `serve` filled with `resolveMultiOrgEnabled()` — i.e. from
9+
`OS_MULTI_ORG_ENABLED`. [ADR-0105 D1] replaced that knob with
10+
`OS_TENANCY_POSTURE`, keeping the boolean only as the fallback
11+
`resolveTenancyPosture()` consults when the posture is unset, and **the runtime
12+
wiring in `serve` already keys off the posture**. So the banner and the server it
13+
describes read two different sources for one fact, and they drifted exactly where
14+
it hurts: booting with `OS_TENANCY_POSTURE=isolated` and `OS_MULTI_ORG_ENABLED`
15+
unset printed
16+
17+
```
18+
Tenancy: single-tenant
19+
Plugins: 40 loaded
20+
…, Organizations, …
21+
```
22+
23+
— the banner claiming single-org one line above the plugin table that proves the
24+
organization wall is up (observed on a real boot in cloud#1020, where the lie was
25+
only caught by hand-comparing the plugin list).
26+
27+
This is not cosmetic. It is the "declared ≠ enforced" class (ADR-0049) landing on
28+
the **diagnostic** surface, which is the worst place for it: a banner that can be
29+
wrong costs every later investigation an extra lap proving whether it is.
30+
31+
**What changes for users.** The row now prints the posture verbatim — `Tenancy:
32+
single`, `Tenancy: group`, `Tenancy: isolated` — sourced from the same
33+
`resolveTenancyPosture()` call the runtime wiring uses. The old `multi-tenant` /
34+
`single-tenant` vocabulary is gone. That vocabulary was itself part of the defect:
35+
tenancy has been a three-valued spectrum since ADR-0105, and a boolean has no
36+
spelling for `group` at all, so a `group` deployment could only ever be
37+
misreported.
38+
39+
**The internal `multiTenant` option is removed, not deprecated.** With the posture
40+
authoritative, a retained boolean could only ever be a field the printer ignores —
41+
and a field that exists but cannot be believed is precisely how this bug was
42+
authored in the first place. `ServerReadyOptions.tenancyPosture` is typed as
43+
`TenancyPosture`, so re-wiring the banner to the legacy boolean now fails to
44+
compile (`resolveMultiOrgEnabled()` returns `boolean`) instead of producing a
45+
plausible-looking wrong line. The interface is package-internal — `format.ts` is
46+
not re-exported from `@objectstack/cli`'s entry point — so no consumer code needs
47+
a change.
48+
49+
Regression-pinned in `packages/cli/src/utils/format.tenancy.test.ts`, which asserts
50+
the printed token **is** `resolveTenancyPosture()`'s answer across the cases that
51+
made the old code wrong: posture set with the boolean unset, posture unset with the
52+
boolean true, both set and contradicting (either direction), the legacy `multi`
53+
spelling, and `group`.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: remove `connector.rateLimitConfig` and the whole outbound rate-limit shape — the engine never existed (#4911, ADR-0049)
6+
7+
`ConnectorSchema.rateLimitConfig` let an author declare an outbound throttle for
8+
their connector — `strategy`, `maxRequests`, `windowSeconds`, `burstCapacity`,
9+
`respectUpstreamLimits`, `rateLimitHeaders` — and nothing anywhere applied it.
10+
This is not the ordinary declared-but-unread case; it is a step worse:
11+
**there is no outbound rate-limiting engine to wire it to.** The only token
12+
bucket the platform owns is `packages/runtime/src/security/rate-limit.ts`, and it
13+
is INBOUND — the dispatcher calls `consume(key)` on a request fingerprint and
14+
answers 429. No connector provider (`connector-rest`, `connector-openapi`,
15+
`connector-mcp`, `connector-slack`) reads the key, and no seam exists that could.
16+
17+
So a well-formed, schema-validated block told the author they had capped their
18+
call rate against a third party's quota, and capped nothing — the false-compliance
19+
class ADR-0049 exists for. With no implementation and no committed roadmap,
20+
`experimental` would be a promise nobody made; **absent** is the honest
21+
disposition. The vocabulary comes back *with* the engine, in one change
22+
(implementation-first — the #4834 / PR #4878 ruling for the plugin-runtime family).
23+
24+
FROM → TO:
25+
26+
| Removed | Replacement |
27+
| :--- | :--- |
28+
| `connector.rateLimitConfig` (key) | **none** — delete it; throttle at the connector provider or upstream gateway |
29+
| `ConnectorRateLimitConfigSchema` / `ConnectorRateLimitConfig` | **none** — importing either is TS2305 in v17 |
30+
| `RateLimitStrategySchema` / `RateLimitStrategy` | **none** — the enum had no other consumer |
31+
32+
**Do NOT substitute `shared`'s `RateLimitConfig`.** That is the INBOUND limiter
33+
(`enabled` / `windowMs` / `maxRequests`) and caps the calls others make to *us*
34+
the opposite direction. #4684 split the two names for exactly this confusion; the
35+
conversion deliberately does not rewrite one into the other, because that would
36+
silently change behaviour rather than losing a no-op.
37+
38+
The retirement kit:
39+
40+
- **Tombstone.** `ConnectorSchema` is not `.strict()`, so a plain delete would be
41+
a silent strip (ADR-0104). `retiredKey()` makes the removal audible in the two
42+
channels an upgrading author hits — `tsc` (the key types `never`) and the parse
43+
(the prescription itself). It reaches `stack.connectors[]` and
44+
`DeclarativeConnectorEntry`, which is `ConnectorSchema.superRefine(…)`.
45+
- **ADR-0087 D2 conversion + D3 chain step** (`connector-rate-limit-config-removed`,
46+
`retiredFromLoadPath`): `os migrate meta --from 16` deletes the key from author
47+
sources and stored rows replay clean. A lossless delete — the block never had an
48+
effect to lose.
49+
- **The shape goes with the key.** `ConnectorRateLimitConfigSchema` and the
50+
`RateLimitStrategySchema` enum it embedded had no other consumer, and an
51+
exported schema with no consumer reads as a capability to whoever finds it
52+
(#3950).
53+
- **#4684's rename is absorbed.** `integration/RateLimitConfig`
54+
`integration/ConnectorRateLimitConfig` and this retirement landed in the same
55+
unreleased major; composed they are a plain delete, so the `RENAMED_DEFS` entry
56+
is removed rather than pointing at a def this build no longer emits.
57+
- Baselines updated deliberately: `json-schema.manifest.json` (−2 defs),
58+
`authorable-surface.json` (−6 def lines; `Connector` /
59+
`DeclarativeConnectorEntry` gain `… [RETIRED]`), `api-surface.json` (−4
60+
exports). `api-surface-signatures.json` is unchanged by construction — it hashes
61+
each `defineX` parameter as TypeScript *prints* it, a reference
62+
(`z.input<typeof ConnectorSchema >`), so key-level narrowing never reaches it.
63+
64+
No runtime behaviour changes — that impossibility is the reason for the removal.

0 commit comments

Comments
 (0)