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
50 changes: 42 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,48 @@ rejects it. Use `position` (distribution), `permission_set` (capability),
`status` and `due_date` directly — they are stored and indexed.
6. **`sharingModel` is mandatory and fail-closed.** Unset means private, and the
publish linter errors on it (ADR-0090 D1/D7). State it deliberately.
7. **Author hierarchy scopes normally; the enterprise edition resolves them.**
`readScope: 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'` (ADR-0057)
are resolved by `@objectstack/security-enterprise`, which enterprise
deployments already carry. Do **not** build an application-level fallback,
and do **not** add `hierarchy-security` to `requires` — that would fail an
open-edition boot. In this open-edition checkout those scopes resolve to
owner-only, so a manager view will show you only your own rows: that is
expected here, and not a bug to chase.
7. **A hierarchy scope REQUIRES `requires: ['hierarchy-security']`. Omitting it
is an author-time hard error, not a silent fallback.**
`readScope`/`writeScope` `'own_and_reports' | 'unit' | 'unit_and_below'`
(ADR-0057) are the hierarchy scopes, resolved by
`@objectstack/security-enterprise`. `objectstack.config.ts` already declares
the capability — you should not need to touch it — and you author the scopes
normally. Build no application-level fallback.

**Grant one without the declaration and `defineStack` refuses to load**, in
`validateHierarchyScopeCapability`. It is not the config file being fussy;
it is the platform closing the exact hole this rule used to tell you to live
with. The platform's own words:

> A stack that uses one MUST declare `requires: ['hierarchy-security']`;
> otherwise the open runtime would silently fail closed to owner-only (the
> metadata would lie, ADR-0049). **This makes that an authoring-time error
> instead.**

Because the check runs inside `defineStack()`, an undeclared scope takes
`validate`, `build` **and** every test that imports the config — so the
symptom is the whole suite going red at once, not one assertion.

**Declaring it does NOT fail an open-edition boot.** Measured on this
checkout with `@objectstack/security-enterprise` **not** installed:
`validate`, `typecheck`, `test` and `build` all exit 0, the kernel logs
`Bootstrap complete`, and `validate` prints exactly one warning naming the
package that provides the capability. **That warning is the expected state
of this repo — do not silence it.** The only two ways to make it go away are
installing the enterprise package (deliberately not done here) and deleting
the declaration (which puts the hard error back).

In this open-edition checkout the scopes then resolve to **owner-only**, so
a manager view shows you only your own rows. That is the edition, not a bug
to chase; verifying manager visibility for real needs an enterprise runtime
and a populated business-unit tree.

⛔ **`org` is not a hierarchy scope** and passes the check with no
declaration at all. That is the trapdoor: it is the nearest thing to hand
when a scope will not load and you want "some visibility for managers", and
it discloses every row in the tenant. Depth for managers is
`unit_and_below`. Narrower than intended is visible and fixable;
`org` is neither.
8. **English is the source language.** Every authored label gets an `en` entry;
`zh-CN` is hand-translated. Do not hard-code display text in a hook or flow.
9. **Metadata first — a handler is the last resort, not the first.** This is an
Expand Down
87 changes: 53 additions & 34 deletions docs/deployment/security.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Security — what ships, and what a real rollout still has to do

Duly declares three positions, three permission sets and no sharing rules. This
page is the other half: the bindings a package is not allowed to make, the
enterprise dependency a real deployment needs, and the two grants that are
currently narrower than the product intends.
page is the other half: the bindings a package is not allowed to make, and the
enterprise dependency a real deployment needs to resolve the manager depth this
package authors.

Nothing here is advice about writing security code. There is no security code:
`definePosition`, `definePermissionSet` and `defineSharingRule` are the whole
Expand All @@ -18,8 +18,8 @@ apply over MCP, and never appears in an audit.
| | `duly_member` | `duly_manager` | `duly_admin` |
|:---|:---|:---|:---|
| Who | everyone who owns duties | anyone with reports or a unit | catalog owners, rollout admins |
| `duly_task` | create / read / edit · read **own** · write **own** | inherited | inherited |
| `duly_duty` | create / read · read **own** · write **own** | inherited | **+ edit** · read **org** · write **own** |
| `duly_task` | create / read / edit · read **own** · write **own** | read **unit_and_below** · write **own** | inherited |
| `duly_duty` | create / read · read **own** · write **own** | read **unit_and_below** · write **own** | **+ edit** · read **org** · write **own** |
| `duly_log_entry` | full control · read **own** · write **own** | inherited | inherited |
| `duly_catalog_item` | read | inherited | **+ create / edit / delete** · write **org** |
| `duly_assignment` | read · read **own** | **+ create / edit** · write **own** | inherited |
Expand Down Expand Up @@ -85,10 +85,14 @@ Assignees still see their own fanned-out `duly_task`, which is the row they work

## An enterprise runtime is a product dependency

The manager model is built on the ADR-0057 depth scopes
(`own_and_reports`, `unit`, `unit_and_below`, `org`). Those are resolved by
**`@objectstack/security-enterprise`**. Without it the platform has no manager
chain and no business-unit tree resolver, so depth collapses to owner-only.
The manager model is built on the ADR-0057 hierarchy scopes
(`own_and_reports`, `unit`, `unit_and_below`) and this package authors them
directly: `duly_manager` reads `unit_and_below` on `duly_task` and `duly_duty`,
and `duly_admin` inherits the task depth.

Those scopes are **resolved** by **`@objectstack/security-enterprise`**. Without
it the platform has no manager chain and no business-unit tree resolver, so
depth resolves to owner-only.

For a real rollout that means:

Expand All @@ -100,31 +104,41 @@ and adding it to `plugins[]`. Manager visibility is not a feature you can verify
on an open-edition checkout; a manager view there shows you your own rows, and
that is the edition, not a bug.

### ⛔ Two manager grants are currently narrower than this table implies

`duly_manager` on `duly_task` and `duly_duty`, and `duly_admin` on `duly_task`,
should read `unit_and_below`. They are authored `own`.

The reason is not caution. `defineStack` **refuses to load** a permission set
carrying `unit`, `unit_and_below` or `own_and_reports` unless the stack declares
`requires: ['hierarchy-security']` — it is a hard error, not the silent fallback
this repo's own notes describe, and it takes `validate`, `build` and every test
that imports the config. This package may not add that declaration
(`objectstack.config.ts` is off-limits to feature work, and a repo rule forbids
it), so the only authorable depths here are `own` and `org` — and `org` would
hand every manager every task in the tenant.

`own` is also exactly what an open-edition runtime would have *resolved*
`unit_and_below` to, so nothing about today's behaviour differs. What differs is
that the declaration is now honest, and an enterprise deployment inherits an
under-grant it can see rather than a grant that quietly never worked.

The three affected grants are recorded machine-readably in
`HIERARCHY_SCOPES_DEFERRED` (`src/security/permission-sets.ts`) and pinned in
both directions by `test/security.test.ts`: widen a grant without deleting its
row and the test fails; delete a row without widening the grant and the test
fails. Tracked as **#46**, which also carries the measurement showing the
"declaring it fails an open-edition boot" belief to be false.
### The declaration, and the warning that is supposed to be there

`objectstack.config.ts` declares `requires: ['hierarchy-security']`. That is
not optional and it is not a rollout step — **it is what makes the depth scopes
authorable at all**. `defineStack`'s `validateHierarchyScopeCapability` refuses
to load a stack that grants `own_and_reports`, `unit` or `unit_and_below`
without it. That refusal is deliberate: it replaces a silent fail-closed to
owner-only with an authoring-time error, so the metadata cannot claim a depth
the runtime will not enforce (ADR-0049).

**Declaring it installs nothing.** On this open-edition checkout, with
`@objectstack/security-enterprise` absent, `validate`, `typecheck`, `test` and
`build` all exit 0 and the kernel boots. What you get is one warning on every
validate and build:

> ⚠ Capability "hierarchy-security" is provided by
> @objectstack/security-enterprise (ADR-0057 hierarchy scopes ship in the
> enterprise edition). Run `pnpm add @objectstack/security-enterprise` and add
> it to `plugins[]`, or remove "hierarchy-security" from `requires`.

⛔ **That warning is the expected state of this repo — do not silence it.** It
is the honest signal that the depth this package declares is not being enforced
in this checkout. The only two ways to remove it are installing the enterprise
package (deliberately not done here) and deleting the declaration, which puts
the hard load error back and forces every manager grant down to `own`.

⛔ **`org` is not a hierarchy scope**, so it loads with no declaration at all.
It is the wrong answer for manager visibility and the reason this section is
long: `org` on `duly_task` hands every manager every task in the tenant.
`duly_admin`'s `org` read on `duly_duty` is a deliberate, separate decision
about obligations, not people's work rows.

`test/security.test.ts` pins both halves — the three depth grants, and the
declaration they require — so neither can be removed without the other going
red.

---

Expand Down Expand Up @@ -195,6 +209,11 @@ pnpm validate # reports "Security: 3 Positions 3 Permissions"
pnpm test # test/security.test.ts asserts every declared scope
```

`validate` also prints the `hierarchy-security` capability warning on an
open-edition checkout. It is expected — see above. A clean run here means the
enterprise package is installed, not that the warning was a problem someone
fixed.

`test/security.test.ts` asserts the **authored** metadata, never resolved rows —
on an open-edition checkout a row count measures the edition, not the
declaration, and would go green on the day someone deleted a scope. To see
Expand Down
29 changes: 20 additions & 9 deletions docs/product/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,23 @@ Every object states `sharingModel` explicitly — unset means private and the
publish linter errors on it (ADR-0090 D1/D7).

Manager visibility comes from permission sets with `readScope`, not from an
org-wide default. The ADR-0057 depth scopes — `own_and_reports`, `unit`,
`unit_and_below`, `org` — are resolved by `@objectstack/security-enterprise`,
which enterprise deployments carry. We author against them directly and build no
application-level fallback.

In an open-edition checkout the resolver is absent and those scopes fall back to
owner-only. That is the expected behaviour of this repo, not a defect: a manager
view here shows you your own rows. Anyone verifying manager visibility needs an
enterprise runtime to see it work.
org-wide default. The ADR-0057 hierarchy scopes — `own_and_reports`, `unit`,
`unit_and_below` — are resolved by `@objectstack/security-enterprise`, which
enterprise deployments carry. We author against them directly and build no
application-level fallback. (`org` is a flat scope, not a hierarchy one; it is
used on the catalog and nowhere near a person's rows.)

Authoring a hierarchy scope **requires** `requires: ['hierarchy-security']` in
`objectstack.config.ts`, which this package declares. Omitting it is not a
silent degradation — `defineStack` refuses to load the stack, which is the
platform deliberately turning the old fail-closed-to-owner-only into an
authoring-time error (ADR-0049: otherwise the metadata would lie).

Declaring the capability installs nothing and breaks nothing. On an
open-edition checkout, with `@objectstack/security-enterprise` absent, every
gate stays green and `pnpm validate` prints one warning naming the package that
provides the capability. **That warning is the expected state of this repo.**

The scopes then resolve to owner-only here, so a manager view shows you your own
rows. That is the edition, not a defect; anyone verifying manager visibility
needs an enterprise runtime to see it work.
44 changes: 32 additions & 12 deletions objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,31 @@ export default defineStack({
// `automation` backs flow and job execution, and materialises declarative
// `connectors:` entries at boot (ADR-0097). The dispatcher lives there.
//
// `hierarchy-security` is deliberately NOT declared here. Manager visibility
// is authored with the ADR-0057 depth scopes ('own_and_reports', 'unit',
// 'unit_and_below', 'org'), which @objectstack/security-enterprise resolves —
// enterprise deployments have it, so no application-level fallback is built.
// This open-edition checkout runs without it and those scopes resolve to
// owner-only; that is the expected open-edition behaviour, not a bug to work
// around. Declaring the capability would make an open-edition boot fail.
requires: ['automation'],
// `hierarchy-security` is REQUIRED, not optional, and declaring it installs
// nothing. Manager visibility is authored with the ADR-0057 depth scopes,
// and `defineStack`'s own `validateHierarchyScopeCapability` refuses to load
// a stack that grants `own_and_reports`, `unit` or `unit_and_below` without
// this entry:
//
// > A stack that uses one MUST declare `requires: ['hierarchy-security']`;
// > otherwise the open runtime would silently fail closed to owner-only
// > (the metadata would lie, ADR-0049). This makes that an authoring-time
// > error instead.
//
// (`org` is NOT a hierarchy scope and passes that check — which is the
// trapdoor: it is authorable without this line and discloses the whole
// tenant. Depth for managers is `unit_and_below`, never `org`.)
//
// Declaring it does NOT fail an open-edition boot. Measured on this
// checkout with @objectstack/security-enterprise absent: `validate`, `test`
// and `build` all exit 0 and the kernel logs `Bootstrap complete`; the
// capability-provider check prints ONE warning naming the package to
// install. That warning is the expected state here — the honest signal that
// these scopes resolve to owner-only until a deployment provides the
// resolver. Do not silence it: the only ways to are installing the
// enterprise package (not wanted in this repo) or deleting this entry,
// which puts the hard error back.
requires: ['automation', 'hierarchy-security'],

plugins: [
new ConnectorRestPlugin(),
Expand All @@ -90,10 +107,13 @@ export default defineStack({
hooks: dulyHooks,
functions: dulyFunctions,

// Security posture. Hierarchy read scopes ('own_and_reports', 'unit',
// 'unit_and_below', 'org') are resolved by @objectstack/security-enterprise,
// which is a HARD product dependency — without it they fail closed to
// owner-only, silently. See docs/product/data-model.md#security-posture.
// Security posture. The hierarchy read scopes ('own_and_reports', 'unit',
// 'unit_and_below') are resolved by @objectstack/security-enterprise, which
// is a HARD product dependency — declared as `hierarchy-security` in
// `requires` above. Without the package installed they resolve to
// owner-only, announced by the capability-provider warning on every
// validate/build rather than silently.
// See docs/product/data-model.md#security-posture.
positions: dulyPositions,
permissions: dulyPermissionSets,
sharingRules: dulySharingRules,
Expand Down
1 change: 0 additions & 1 deletion src/security/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export {
DULY_CATALOG_APPLY,
DULY_CATALOG_SYNC,
DULY_TASK_UPDATE_STATUS,
HIERARCHY_SCOPES_DEFERRED,
} from './permission-sets.js';

export const dulyPositions = [MemberPosition, ManagerPosition, AdminPosition];
Expand Down
Loading
Loading