Skip to content

Commit 5a40a42

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-8133-spec-shared-chunks
2 parents 19a8800 + c61f75d commit 5a40a42

132 files changed

Lines changed: 9573 additions & 2239 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the objectstack-automation capability table says a declared webhook or auto-launched flow is refused without its `requires` pair, not silently undelivered
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The 〈Prerequisite — declare the capabilities your nodes need〉 table in `objectstack-automation/SKILL.md` said that without `webhooks` in `requires` "nothing [is] delivered outbound", and that without `triggers` "flows register, never fire". Neither is what `defineStack` does: a declared webhook while `requires` omits `webhooks`, `realtime` or both is refused with `STACK_WEBHOOK_CAPABILITY_REQUIRED` (422), and an auto-launched flow while `requires` omits `triggers`, `automation` or both is refused with `STACK_TRIGGER_CAPABILITY_REQUIRED` (422) — at `os validate`, `os build`, `os dev` and `os serve` alike. The two rows now name the pair each surface owes and the refusal code; the lead-in says the refusal is at load time and that neither token implies the other. The `requires: [...]` example block under the table is folded into the lead-in to keep the skill within its token ceiling.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/service-datasource': patch
4+
---
5+
6+
fix(rest): an unknown resource answers `404 RESOURCE_NOT_FOUND` at the package draft and commit-trail doors, the flow runs list and delete, and the datasource by-name write doors, as their read siblings already did
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. What changes is the HTTP answer eight REST doors give for a resource name that does not exist. -->
12+
13+
**BREAKING** (a served-set narrowing) for a caller that sends one of these doors a package id, flow name or datasource name that does not exist. Five of them used to answer such a request `200` with an empty result. All eight now refuse it.
14+
15+
Eight doors answered a request about a resource that does not exist as if it were an empty or a bad request, while their sibling doors answered `404` for the same name. An SDK or AI caller could not tell "no such thing" from "nothing yet". Each now answers `404 RESOURCE_NOT_FOUND`, with the status, code and message its sibling gives for the same name. The answer before and after, per door:
16+
17+
- `POST /api/v1/packages/:id/publish-drafts`, for a package the registry does not hold: `200 { success: false, outcome: 'nothing_to_publish' }` → `404 RESOURCE_NOT_FOUND "Package '<id>' not found"`. That is the answer of `GET /api/v1/packages/:id`.
18+
- `POST /api/v1/packages/:id/discard-drafts`: `200 { success: true, discardedCount: 0 }` → the same `404`.
19+
- `GET /api/v1/packages/:id/commits`: `200 { commits: [] }` → the same `404`.
20+
- `GET /api/v1/automation/:name/runs`, for a flow the engine does not hold: `200 { runs: [], hasMore: false }` → `404 RESOURCE_NOT_FOUND "Flow '<name>' not found"`. That is the answer of `POST /api/v1/automation/:name/trigger`.
21+
- `DELETE /api/v1/automation/:name`, for a flow that neither the engine nor the metadata store holds: `200 { name, deleted: true }` → the same `404`.
22+
- `PATCH /api/v1/datasources/:name`, for a datasource that does not exist: `400 DATASOURCE_ADMIN_ERROR "Datasource '<name>' not found."` → `404 RESOURCE_NOT_FOUND 'Datasource "<name>" does not exist.'`. That is the answer of `GET /api/v1/datasources/:name`, word for word.
23+
- `DELETE /api/v1/datasources/:name`: the same `400` → the same `404`.
24+
- `POST /api/v1/datasources/:name/migrate-credential`: the same `400` → the same `404`.
25+
26+
Unchanged: a resource that exists keeps its answer. An empty page for a flow that has not run, `nothing_to_publish` for a package with nothing pending, and `400 DATASOURCE_ADMIN_ERROR` for a code-defined datasource all stay as they were. Drafts or commits the store holds for an id the registry no longer lists are still published, discarded or listed. A stored flow the engine does not hold is still deleted. A failed ADR-0045 visibility flip still answers `200` with its `unhideError`.
27+
28+
**What changes for you.** `ObjectStackClient` already throws on any non-2xx answer. `packages.publishDrafts`, `packages.discardDrafts`, `packages.listCommits`, `automation.listRuns` and `automation.delete` now reject with the not-found message for an unknown id, instead of resolving an empty result. Treat that rejection as "no such resource". An embedder calling `DatasourceAdminService.updateDatasource`, `removeDatasource` or `migrateCredential` directly now gets an error carrying `code: 'RESOURCE_NOT_FOUND'` and `status: 404`. Its message is now `Datasource "<name>" does not exist.`.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/service-storage': patch
4+
---
5+
6+
fix(rest)!: an anonymous public-form submission carries no file
7+
8+
`POST /api/v1/forms/:slug/submit` (and its environment-scoped twin) now refuses a submission that carries a value in a field whose declared type is a file type (`file`, `image`, `avatar`, `video` or `audio`, the spec's `FILE_REFERENCE_TYPES`). The refusal comes before anything is written. It is the door's existing envelope, `400 VALIDATION_FAILED`, with one `fields[]` entry per file field, coded `invalid_value` (the ADR-0114 field catalog) and naming the field by its label. A missing `required` field in the same submission is reported in the same envelope. An empty or absent file field submits exactly as before. Only the fields the form declares are judged, so the refusal names nothing the form does not already publish.
9+
10+
This is the server half of the rule "an anonymous public-form submission carries no file". A visitor who needs to provide a file registers and signs in, then submits through a signed-in surface that uploads through the authenticated upload doors. The anonymous door has no upload door of its own, and none is added. The protocol half, refusing at publish a guest-facing form that declares a file field, is separate. This door also judges forms published before that refusal exists.
11+
12+
`@objectstack/service-storage`: the file-reference claim path now looks a referenced file up within the organization the write acts in. It uses the same channel that already stamps a copied file for that organization, so the driver's tenant scope for the posture in force decides what the lookup sees. A referenced file id outside that scope is kept as written and is neither claimed nor copied, exactly as an id the platform does not manage. A write that carries no organization, and the `single` posture, look files up as before.
13+
14+
Clause-②: no (narrowing) — the anonymous public-form submit door refuses a request shape it used to accept (a value in a declared file field), and the file-reference claim path reaches only files within the writing organization's tenant scope. Nothing is widened.
15+
16+
Metadata-protocol: none (a request's handling at an anonymous door and a write path's file bookkeeping; nothing a metadata author writes changes. The publish-time refusal of a guest-facing form with a file field is the protocol half, and ships separately)
17+
18+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is how the anonymous public-form submit door handles a request body (a value in a declared file field is refused) and which files the file-reference claim path can reach for a write (those within the writing organization's tenant scope). The remedy is a change of how a visitor submits (register and sign in to provide a file), never a rewrite of a package source or a stored row. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
19+
20+
**BREAKING** for a public form that collected files anonymously, shipped as `patch` because the metadata protocol does not move.
21+
22+
- **Before.** A value in a file field the form declares was written with the anonymous submission.
23+
- **Now.** That submission is refused with `400 VALIDATION_FAILED`, naming each file field that carries a value. A submission that leaves every file field empty is accepted as before.
24+
- **Fix.** A public form that collected files: have the submitter register and sign in, then provide the file from their account. Remove the file field from the guest-facing form, or leave it for the visitor to leave empty.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(plugin-approvals): an approval step's `excludeSubmitter: true` keeps the submitter from approving their own request
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Before.** `excludeSubmitter` was declared on the approval node config and its describe opened with `[PLANNED — not yet enforced]`: the approval engine parsed it and read nothing, so a category manager who submitted their own admission was step 1's `field` approver and approved it.
12+
- **At the open.** The submitter (the run's trigger user, `sys_approval_request.submitter_id`) is taken off the resolved slate, whatever approver type produced them, by their user id or their account's email, an out-of-office hop that lands on them included. Everyone else stays on it, per_group tags included. A slate this empties takes the node's `onEmptyApprovers` route: the organization's administrators by default, the declared `fallbackApprovers` under `'fallback'`, or the `NO_APPROVERS` refusal under `'fail'`, which now names the exclusion. The fallback slate and the administrators' slate are screened the same way, so an organization whose only administrator is the submitter opens the request on the slate that names no one.
13+
- **On the record.** A request whose slate lost its submitter carries a system note on its timeline naming the route it took (the remaining approvers, the fallback approvers, the administrators, or no one), rendered in the reader's language by the approvals door. The administrators' routing note says the only approver was the submitter instead of "no approver resolved to a person".
14+
- **After the open.** An approval decided by the submitter is refused with `FORBIDDEN`, the privileged override included; their rejection stands. A reassign to the submitter, or to a position only they hold, is refused with `VALIDATION_FAILED`. An SLA escalation that reassigns leaves them off the new slate, and one whose only target is the submitter is not applied: the pending approvers keep the request and are told the SLA lapsed.
15+
- **Spec.** The describe drops the `[PLANNED — not yet enforced]` marker and its last sentence; nothing else in the schema changes.
16+
- **Unchanged.** With the option off or omitted (the default), every slate, timeline, decision, reassign and escalation behaves as before.
17+
18+
What to do: nothing to migrate. A step that set `excludeSubmitter: true` now enforces it; an `expression` approver or validation rule added by hand to keep the submitter off can be removed.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
feat(spec,lint)!: a permission set can no longer declare `allowDelete: false` beside `modifyAllRecords: true`
7+
8+
`modifyAllRecords` (Modify All Records) is the write bypass for edit, delete and transfer, as Salesforce's Modify All is. The permission evaluator grants delete on the bypass bit and never reads `allowDelete`, so an object-permission entry declaring `allowDelete: false` beside it parsed, was stored, and let its holder delete every record. The runtime does not change. What changes is what an author can write:
9+
10+
- `ObjectPermissionSchema` refuses an explicit `allowDelete: false` in an entry that sets `modifyAllRecords: true`, with one `custom` issue at `objects.<object>.allowDelete` (the wildcard `*` entry included). The message names the route for "edit every record, delete none".
11+
- An entry that sets `modifyAllRecords: true` and omits `allowDelete` now parses to `allowDelete: true` instead of `false`. That is what the evaluator already granted, and it keeps the parse idempotent: the output of `definePermissionSet` still parses when `defineStack`, a build or a save parses it again.
12+
- `os lint` reports the pair as `security-modify-all-delete-withheld` (error) on a raw config that has not been through the schema. `os explain security-modify-all-delete-withheld` prints the route.
13+
- The response-side `EffectiveObjectPermissionSchema` is unchanged and still accepts the pair.
14+
15+
"Edit every record, delete none" is a `criteria` sharing rule with `accessLevel: 'edit'` shared with the position that holds the set, beside `allowEdit: true` and `allowDelete: false` with no `modifyAllRecords`. An edit share opens update and never delete (ADR-0111 D3). The object needs an owner field and an owner-write default (`private` or `public_read`), and the rule's `condition` must be a field predicate every row satisfies, such as `record.id != ''`: a literal `true` lowers to an empty filter, which the sharing service never serves (it logs the rule as not served at boot).
16+
17+
Stored rows and older built artifacts: every earlier parse filled an omitted `allowDelete` with `false`, so the pair also sits where nobody wrote it. The ADR-0087 D2 conversion `permission-modify-all-allow-delete-false-to-true` rewrites it to `true` when a stored row is rehydrated and when a built artifact inside its declared-floor window is loaded. That changes no access decision. It is retired from the load path, so a source that still declares the pair is refused, not rewritten. The D3 entry `permission-modify-all-allow-delete-false-refused` carries the judgment for `os migrate meta`.
18+
19+
Clause-②: no (narrowing) — the object-permission schema refuses a shape it used to accept (`allowDelete: false` beside `modifyAllRecords: true`). Nothing is widened.
20+
21+
Metadata-protocol: narrowed (an object-permission entry may no longer declare `allowDelete: false` beside `modifyAllRecords: true`)
22+
23+
Ruling-ref: 6377890121 (letter A, under 6377842908); retriage 6382861731
24+
25+
<!-- adr-0087: registered permission-modify-all-allow-delete-false-refused, permission-modify-all-allow-delete-false-to-true -->
26+
27+
**BREAKING** for an author who declared the pair.
28+
29+
- **Before.** `{ allowEdit: true, allowDelete: false, modifyAllRecords: true }` parsed, and the holder could delete every record.
30+
- **Now.** The entry is refused at `objects.<object>.allowDelete`.
31+
- **Fix.** If the set should delete every record, delete `allowDelete` from the entry. If it should edit records it does not own but delete none, remove `modifyAllRecords` and add a sharing rule: `{ type: 'criteria', name, object, condition: "record.id != ''", accessLevel: 'edit', sharedWith: { type: 'position', value: '<position>' } }`.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: a code package's action is stored as its `ActionSchema` parse, so `requiresFeature` takes effect on bound actions
6+
7+
Clause-②: no (narrowing) — `SchemaRegistry.registerItem`, and with it `ObjectQL.registerApp` and every door that registers a code package's items, now refuses with `422 INVALID_METADATA` an `action` body that does not satisfy `ActionSchema`. Before, it registered that body as written. Nothing is widened.
8+
9+
Metadata-protocol: none (`ActionSchema` is unchanged and no key is added, retired or re-shaped; every body the registry now refuses already fails the same schema at `defineStack`, `os validate` and the metadata save door)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: `ActionSchema` is byte-unchanged, and no spec key, authorable spelling, stored row or stored shape is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no old-to-new mapping to record. What changes is one runtime door converging on the existing contract: the schema registry now stores a code package's action as its parse and refuses one that does not parse. Every body it refuses was already off-contract, and refused by the authoring doors, before this change. The remedy is a correction of that body against the issues the refusal lists, not a conversion between two contract versions. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
The schema registry now parses every `action` item a code package registers through `ActionSchema` before storing it. The stored item is the parsed body. Before this, an action that reached the registry without a parse was stored as authored. Examples are a programmatic `registerApp`, a nested plugin's manifest, a `strict: false` stack, and a raw plugin manifest. Such an action was then attached to its target object unparsed. Its `requiresFeature` (and `requiresMembershipReach`) was never lowered into `visible`, so the declared gate did nothing. A platform object's own actions were already lowered.
14+
15+
What the parse does to such an action, as with any parsed action:
16+
17+
- it lowers `requiresFeature` / `requiresMembershipReach` into `visible`;
18+
- it fills schema defaults;
19+
- it wraps a CEL string shorthand in `{ dialect, source }`.
20+
21+
An action a strict `defineStack` already parsed is stored unchanged.
22+
23+
**BREAKING** for a package that registers an action body `ActionSchema` does not accept.
24+
25+
- **Before.** The registry stored such a body as written, and served it with any declared gate inert.
26+
- **Now.** Registration is refused with `INVALID_METADATA` (422). The error names the action and its package, and `issues[]` gives the path and message of each failure. The registration that carried it fails.
27+
- **Fix.** Correct the action as `issues[]` describes. An action that passes `os validate`, or a strict `defineStack`, is unaffected.

0 commit comments

Comments
 (0)