Skip to content

refactor(BA-7297): migrate the manager's pass-through actions to ops-generic wiring - #13708

Open
HyeockJinKim wants to merge 53 commits into
mainfrom
BA-7297-passthrough-sweep
Open

refactor(BA-7297): migrate the manager's pass-through actions to ops-generic wiring#13708
HyeockJinKim wants to merge 53 commits into
mainfrom
BA-7297-passthrough-sweep

Conversation

@HyeockJinKim

@HyeockJinKim HyeockJinKim commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Moves the manager's pass-through actions onto ops-generic wiring: an operation whose
service only forwarded a spec to a repository that only forwarded it to db_source now
carries the spec on the action and runs against the generic ops services, and the two
forwarding layers go. Fifteen domains, ninety-six actions.

The sweep kept turning up things that had to be settled before the next domain could
move, so the branch carries those too:

  • Framework — a global get factory, a global create composed with the field rows it
    owns, RESTORE wired through the v2 bases, and the many-row writes renamed for their
    failure mode (atomic_* raises, partial_bulk_* answers per item).
  • Declarations — an operation is named for what it does to the row. A write that
    sets a lifecycle flag is a delete; one that removes the row is a purge. Fourteen
    actions were declaring the wrong one, including four that removed rows while claiming
    a soft delete. Modify becomes Update everywhere, matching the operation the
    action already declared.
  • Projections — a data/ type that mirrors one row may not nest another entity.
    Notification rules name their channel by id; model cards no longer carry their
    resource requirements. Both compose one layer up instead, which is what freed those
    domains for the v2 specs at all.
  • Two security fixes — a resource policy node reached the keypairs sharing it, so
    any authenticated user could enumerate other users; and the legacy schema loaded any
    named policy with no role check. Neither was introduced here.
  • Tests — eleven files referenced modules the migrations had deleted, hidden because
    pants check --changed-since never reached files this branch had not edited. Repo-wide
    pants check :: finds them; that is the gate this branch used from then on.

Test plan

  • pants check :: and pants lint :: pass repo-wide
  • test_registry_catalog.py — every v2 action is wired, names are unique and snake_case
  • Component suites for the migrated domains (storage, object storage, vfs storage,
    notification, resource policy, app config, prometheus preset, error log)
  • Model card repository suite — requirements read and write through their own table
  • Verify against a live server that the migrated reads and writes still answer, and
    that minResource resolves on demand in GraphQL

Resolves BA-7297


📚 Documentation preview 📚: https://sorna--13708.org.readthedocs.build/en/13708/


📚 Documentation preview 📚: https://sorna-ko--13708.org.readthedocs.build/ko/13708/

Copilot AI balanced review requested due to automatic review settings August 11, 2026 18:25
@HyeockJinKim
HyeockJinKim requested a review from a team as a code owner August 11, 2026 18:25
@github-actions github-actions Bot added the size:XL 500~ LoC label Aug 11, 2026
HyeockJinKim added a commit that referenced this pull request Aug 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@github-actions github-actions Bot added comp:manager Related to Manager component comp:common Related to Common component require:db-migration Automatically set when alembic migrations are added or updated size:L 100~500 LoC area:docs Documentations and removed size:XL 500~ LoC labels Aug 11, 2026
Comment on lines +50 to +58
with pytest.raises(PermissionDeniedError):
await user_registry.operations.append_error_log(
AppendErrorLogRequest(
severity=ErrorLogSeverity.WARNING,
source="test-user",
message="Test warning from user",
context_lang="python",
context_env="{}",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the change of results of the same action okay?

HyeockJinKim and others added 17 commits August 16, 2026 11:21
…ps-generic wiring

Every retention_policy operation only forwarded a repository spec, so the domain
now wires the generic ops services directly: the actions carry v2 specs
(`RetentionPolicyCreator` / `Updater` / `Purger` / `Searcher`) and the service,
repository and db_source are gone.

`delete` becomes purge-shaped — a retention policy carries no lifecycle column, so
removing one has always been the row leaving the table; it keeps its own
`action_name` so the audit history does not merge with `purge`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ops-generic wiring

Both services only forwarded a repository spec, so the catalog now wires the generic
ops services directly — reads through the public gate (`public_get_ops` /
`public_search_ops`), writes through the SUPERADMIN one. The service, admin service,
repository, admin repository and db_source are gone.

`delete` becomes purge-shaped: the table carries no lifecycle column, so removing a
row has always been a hard delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The search executed SQL inside the service — a layer violation with no repository
behind it. The select and the row-to-data assembly move into a `Searcher` spec and
the action wires straight to `global_search_ops`, so the service disappears without
a repository being introduced in its place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The keypair / user / project resource policies key on `name` — what `keypairs`,
`users` and `groups` reference — so the primary key stays where it is. What they
lacked is an `EntityID`: the v2 action layer identifies every entity by a UUID,
and `ProcessorGroup` is bounded on `EntityData`, so a name-keyed row could not be
wired to the generic ops services at all.

`uuid` is added as a unique alternate key alongside the name, the same shape
`resource_slot_types` already carries, and the `data/` types report it as their
`entity_id()`. Existing rows are backfilled by the column default, so the column
is NOT NULL from the start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns to ops-generic wiring

Every operation only forwarded a repository spec, so the domain wires the generic
ops services directly and the service, repository and their legacy specs are gone.
The name stays the primary key, so the querier, updater and purger address the row
by it while the new `uuid` answers `entity_id()`.

Adds the `global_get_ops` factory: `GetGlobalOpsAction` already existed for a
SUPERADMIN-gated read of a name-keyed global row, but only the `public_*` half of
the get axis had a factory — the search axis already carried both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to ops-generic wiring

Five of the six operations only forwarded a repository spec and now wire to the
generic ops services. The service keeps one method: resolving the caller's own
policy joins through `users`, which a `DataLookup` cannot express — a lookup spec
stays on one table by design — so that read keeps its repository method, and the
auth service's `get_by_name` keeps the other.

The repository test now covers the join that justifies the repository, and the
service test covers the one method left on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns to ops-generic wiring

Five of the six operations only forwarded a repository spec and now wire to the
generic ops services. The service keeps one method: resolving the caller's own
policy joins through `keypairs` and filters on the keypair being active, which a
`DataLookup` cannot express — a lookup spec stays on one table by design.

The creator drops `max_quota_scope_size` / `max_vfolder_count` / `max_vfolder_size`:
they are not columns of this table and `build_row` never wrote them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`FieldEntityCreator.build_row(owner_id)` already had the shape a just-created
parent's id calls for, and `V2WriteOps` already carried both writes on one session —
what was missing was the composition above it, so a parent-plus-children create had
no ops-direct path and stayed on the legacy `DependentCreatorSpec`.

Adds that composition at each layer it was missing: `EntityWithFieldsResult` (the
repository answer, a dataclass rather than a pair so the two halves cannot be read
the wrong way round), `OpsRepository.create_global_entity_with_fields`,
`GlobalCreateWithFieldsService`, `CreateGlobalWithFieldsOpsAction` and the
`global_create_with_fields_ops` factory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bulk_` covered two contracts at once. `bulk_create_*` flushed every row in one go
and raised on the first failure; `bulk_purge_*` and `bulk_update_data` isolated each
item in a savepoint and answered per entity. Only the argument shape (`Sequence` vs
`Mapping[EntityID, …]`) and the return type (`list` vs `BulkResultWithFailures`) said
which was which.

`Bulk` was also the wrong word on the create side: none of the four bulk creates is
bulk-shaped — their target is one scope, one owner, or the system, so they are
scope-, single-entity- and global-shaped. Only the purge and update paths carry
`BaseBulkAction`, where the caller names the entities and each is answered for.

So the failure mode is now written out, with no unmarked default:

    atomic_create_*          raises; the run records one failure
    partial_bulk_purge_*     per-entity verdicts; the run itself succeeds
    partial_bulk_update_*

The rename runs through the ops methods, `OpsRepository`, the ops-backend action
axis, the combined bases, the generic services and the factories. Shapes, result
types, the condition-selected `batch_*` family and the legacy providers are
untouched; the factories have no callers yet, so no wiring site moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soft delete keeps turning up — role preset today, vfolder, project and user
next — and each of them meets the same three-layer split with nothing written
down: ops executes an UPDATE, the action reclassifies the run as a DELETE, and
the spec is the only place the transition can be fenced off.

The rules go in `models/specs/AGENTS.md` (the lifecycle column stays off the
general updater; delete and restore get their own updaters with constant
`build_values()`) and `actions/AGENTS.md` (a soft delete inherits a `Delete*`
base and never reaches the transition through an update-shaped action).

The background goes in the matching `KNOWLEDGE.md` files: why the guard has to
sit in the spec rather than in ops, why the delete services are identical to
their update siblings on purpose, and — from the rename that preceded this —
why the atomic/partial failure mode is named rather than passed as an argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ActionOperationType.RESTORE` already existed, mapped to the soft-delete
permission, and was written up as a deliberate split — the audit label is
restore, the permission checked is soft-delete. What was missing was anywhere to
declare it: the v2 ops bases stopped at `Delete*`, so a restore had no base to
inherit and fell back to declaring `UPDATE`, which loses the distinction exactly
where it was supposed to be visible.

Adds `RestoreSingleEntityOpsAction` and `RestorePartialBulkOpsAction` with the
services and factories behind them. Both execute as an update, like their delete
siblings — the DB operation is an UPDATE either way and the direction is the
updater's business.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every one of the eleven operations only forwarded a repository spec, so the
domain wires the generic ops services directly and the service, repository and
db_source are gone — the RBAC-specific ops provider goes with them.

The create was the reason this domain had stayed on the legacy path: it writes a
preset and its permission rows together, which the v2 specs could not express
until the with-fields composition landed. `RolePermissionPresetCreator` is a
field creator of its preset, exactly as the entity judgment already said, and the
create carries both specs so the two writes share a transaction.

Soft delete gets the split the conventions now call for: `RolePresetUpdater`
carries no `deleted` field, and `RolePresetSoftDeleteUpdater` /
`RolePresetRestoreUpdater` write the constant. Restore declares `RESTORE` rather
than the `UPDATE` it used to.

`bulk_add_permissions` becomes atomic — a preset granting a subset of what the
caller asked for is worse than one that refused — while removals stay per-entity,
where the caller named the rows and each one's fate belongs in the answer. The
`role_name_template` check moves out of the write and into an action validator,
so a broken template is refused before a transaction opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s-generic wiring

All five operations only forwarded a repository spec, so the domain wires the
generic ops services directly and the service is gone.

`resolve_by_name` becomes the first `lookup_ops` consumer: the variant name is a
unique key on one table, which is exactly what `DataLookup` is for, and the
lookup shape reports the resolved id on the audit trail where the search it used
to ride on reported nothing.

The repository keeps the two reads that live outside the action layer — sokovan's
deployment executor reads a variant by id, the model-serving service by name —
and loses everything the ops path now covers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-generic wiring

Six of the eight operations only forwarded a repository spec and now wire to the
generic ops services. The service keeps the two presigned-URL paths, which reach
S3, and moves to the v2 global base behind the service-backed factory.

`delete` becomes `purge` in name as well as shape. The table carries no lifecycle
column — the repository issued a plain `sa.delete` — so declaring `DELETE` had it
checked against the soft-delete permission instead of the hard-delete one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…neric wiring

Six of the ten operations only forwarded a repository spec and now wire to the
generic ops services. The service keeps the four quota-scope paths, which reach
the storage proxy, and moves to the v2 global base behind the service-backed
factory.

`delete` becomes `purge` in name and shape, as object_storage's did — the table
has no lifecycle column, so `DELETE` had it checked against the soft-delete
permission.

`get` branched on which key the caller supplied, reading by id or by name. The
name half becomes its own lookup action, which is the shape that branch exists to
absorb; the adapter and the download handler each call the one they mean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`NotificationRuleData` carried the whole channel, so the row conversion read a
relationship, so `NotificationRuleRow` needed one, so every create, update, get
and search re-selected the rule with the channel eagerly loaded. One choice in
the data type reached all the way down into four queries.

A row projection mirrors one table. The rule now carries `channel_id`, the
relationship and its join-condition helper are gone, and the four eager loads
with them. Both id fields become typed (`NotificationRuleID` /
`NotificationChannelID`) and the two data types become frozen, as the data layer
requires.

Two reads genuinely need both tables and say so in the statement: dispatch joins
the channel to filter on its `enabled` flag and returns the pair as
`MatchingNotificationRuleData` — a composite the repository assembles, not a row
projection — and `validate_rule` reads the channel by the id the rule names.

The GraphQL rule keeps a `channel` field, now a resolver over the channel
dataloader that already existed: a client that does not ask for it does not pay
for the read, and one that asks across a page gets them batched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`notification_rules.channel_id` is NOT NULL, so a rule without a channel cannot
exist and the resolver has no reason to answer null — which also keeps the field
from becoming a breaking schema change. A miss now raises
`NotificationChannelNotFound` rather than being reported as absence.

`channelId` is a field added after its parent type, so it declares its own
`added_version` instead of inheriting the type's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HyeockJinKim and others added 26 commits August 16, 2026 11:21
Every operation forwarded a spec through the service and the repository without
touching it, so both layers go and the actions carry the v2 specs.

The create was scope-shaped, but the scope it named was always the global one with
an empty id — a global action wearing a scope. It says so now, which moves its gate
from a scope-chain resolution that only the global scope could satisfy to the
SUPERADMIN check the rest of the catalog's writes already use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-generic wiring

Four of the seven operations forwarded a spec without touching it; those move to ops
and the delete becomes a purge, the table carrying no lifecycle column.

Three stay. Preview and execute call Prometheus. The modify stays for a subtler
reason: `filter_labels` and `group_labels` live together in one JSONB column, so a
partial update has to read the stored value and merge before it writes. That is a
read-then-write, not a forward — the earlier survey had it marked pass-through.

The metric repository resolves preset ids while evaluating utilization queries and
has no action to run, so the db_source keeps a search for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…share it

`myKeypairResourcePolicyV2` hands any authenticated caller the same node type the
admin queries return, and that node carries a `keypairs` connection searching every
keypair assigned to the policy. Since most users sit on `default`, one query walked
from one's own policy to every other user's access key, admin flag and email. The
adapter's docstring already claimed a superadmin gate on that field; it was never
there. It is now.

The legacy schema had the same shape by a different route: `keypairResourcePolicy`
and `userResourcePolicy` read one's own policy when called without a name, but a
name — anyone's — was loaded with no role check at all, while the sibling list
resolvers next to them branch on role carefully. Naming a policy now needs the roles
that may already list them.

Neither changes what a user sees about their own policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixteen actions said `Modify` while the operation they declare is `UPDATE`, and the
three resource policies had already drifted the other way — class `Modify*`, audit
name `admin_update_*`. One verb for one operation, so `Modify` follows the operation
it performs, through the action classes, their files, the service methods and the
processor attributes.

The names clients see do not move: the legacy `modify_domain` / `modify_group` /
`modify_user` mutations, `admin_modify_prometheus_query_preset`, and the REST routes
all keep their spelling. Only the handler and adapter methods behind them follow the
attribute they call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t a scope of its own

The previous fix put `check_admin_only()` on the GraphQL field. That gate only covers
the one resolver, and the adapter method behind it stayed reachable — so the hole
would reopen the next time something else called it.

The connection now runs the admin keypair search narrowed by a resource-policy filter.
`AdminSearchKeypairsAction` moves onto the global base, whose processor prepends the
SUPERADMIN check itself: the gate travels with the action, no caller can skip it, and
no configuration can turn it off.

That last part is what made the scope path unsafe rather than merely indirect.
`SearchKeypairsByResourcePolicyAction` did carry an RBAC scope validator, but that
validator returns early when RBAC enforcement is disabled — so with enforcement off,
every authenticated caller passed. Its scope was also ungrantable: keypair resource
policies appear in no scope-entity combination, so no role could have authorized it
even with enforcement on. The action, its service method, its repository read and its
operation scope all go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two of the ninety-odd names this branch declared did not follow the rest.

The service catalog search recorded `admin_search_service_catalogs`, but the
qualifier marks a path that has a counterpart, and it has none — every other
super-admin-only global catalog (login client types, retention policies, runtime
variants, resource slot types) records its search unqualified. So does this one now.

Error logs had it the other way round. `list_error_logs` was the read that narrows
rows by the caller's role, while `search_error_logs` was the one that returns every
row; but `list_` elsewhere in the codebase means "unpaged", not "scoped", and both of
these page. They become `search_error_logs` and `admin_search_error_logs`, which is
the pairing the app-config domains already use, and `list_` keeps its one meaning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on to ops-generic wiring

Registering a namespace and the two reads over the table forwarded specs unchanged;
they move to ops. Reading one storage's namespaces was shaped as a get, but what
comes back is a list and the storage id selects it rather than identifying a row — it
is a search with a condition, and says so now.

Two stay. Unregistering addresses its row by `(storage_id, namespace)` while the purge
specs key on a single primary value, so it keeps its service; it does declare `PURGE`
rather than `DELETE`, the table having no lifecycle column. The grouped read assembles
a dict of namespaces per storage, which is an aggregation rather than a page of rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…thout carrying them

`ModelCardRow.to_data()` read `resource_requirement_rows`, so producing a
`ModelCardData` meant loading a child table. That forced the relationship, forced an
eager load on every read that returned a card, and put the whole domain out of reach
of the single-entity read/write specs — not just the writes, but the get and the
searches too.

The requirements move out of the projection. The row carries its own columns, the
relationship goes, the creator names the child rows separately for the write path to
insert, and the update's re-read after syncing them is no longer needed because
nothing implicit reads them any more.

What renders them asks for them, and asks for every card at once. REST fills the
field with one extra batched read before answering, so its response is unchanged.
GraphQL resolves it as a field, so a query that does not select `minResource` never
reads that table — which is most of them, since the only code that consumes the
requirements is `available_presets`, and that has always joined the table directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Now that the card's projection no longer reaches into its requirements table, the
create is expressible as an entity spec. `ModelCardCreator` says in the spec what the
call site used to say in an RBAC element reference: a card is its own scope, and it
joins the model-store project it is registered in.

Not yet wired — the actions still run the legacy creator. It lands here so the
follow-up that moves create, search and the project-scoped search onto ops starts
from a spec that already typechecks against the entity family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The migration re-chains onto main's new head; the two import edits are what the
conflict resolution left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: octodog <mu001@lablup.com>
Moving these actions onto the global processor attached a SUPERADMIN gate that
the legacy processor never had, so routes their own registry declares as
auth_required started answering 403 to every regular user.

The route registries are the authority: a domain can serve two surfaces from one
processor -- storage answers an auth_required v1 surface and a
superadmin_required v2 surface -- so the processor carries the weaker gate and
the route middleware keeps the stronger one. Reads move to the public processor,
which demands a caller but not a role; writes stay behind the gate.

Three domains had no test covering a non-admin caller at all, since every
existing case authenticates as an admin. A read/write access pair now pins the
contract for the query-preset catalog.

Also repair the global purge path: it rebuilt the deleted row by calling the row
class, which fails on any row whose __init__ narrows to the caller-supplied
columns -- a server-generated one arrives as an unexpected keyword. It now maps
the RETURNING columns the way insert and update already did.

error_log keeps the gate by decision: its recording and per-user reads become
superadmin-only, and the tests state that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An error log belongs to whoever hit it, but the sweep had filed the domain as
global state, so recording one demanded super-admin. The row carries a user FK,
an is_cleared flag the owner dismisses it with, and a read that hides cleared
rows from that owner and shows them to an admin -- none of which a global entity
has.

Recording and reading now target the owning user's scope, so the permission
question is whether the caller may act on that user's error logs rather than
whether they administer the installation. Clearing targets the log itself: by
then the row exists to name, and it is a soft delete, so it carries an updater
whose values are a constant and an operation_type that records the run as a
delete rather than an edit.

The actions no longer carry is_admin, is_superadmin or user_domain. Who the
caller is comes from the request context; an action that takes its own caller's
role as an argument is taking a claim it cannot check. The REST handler branches
between the scoped read and the whole-table read instead, which is where the two
gates were always distinguishable.

That leaves the domain with no service and no hand-written repository methods.
The row also loses its constructor: created_at moves to the shared mixin, so no
writer can set it.

Ownership was enforced by a WHERE clause in the repository, which is domain
judgment living below the layer that owns it. It now rests on the permission
layer, whose rows for this entity type are seeded separately -- until then a
non-super-admin caller is refused. The component suite cannot see either state
because it replaces the virtual-scope validators with mocks; the ownership test
that depended on the old WHERE clause is dropped rather than left asserting a
guard that no longer runs there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removal addressed a namespace by the (storage, namespace) pair it was registered
under, which no purge spec can express -- they key on a single primary value --
so the operation kept a service and a hand-written repository method to work
around the shape.

Translating the pair into an id is a read, so it becomes one: a lookup resolves
the pair, and the purge takes the id like every other purge. The unique
constraint on the pair is what makes that resolution single-valued, and the
lookup family already rejects a second match rather than answering with an
arbitrary row.

The grouped read goes the same way. Grouping namespaces by storage was a
repository method returning a dict, which is the caller's shape rather than the
table's; the deprecated bucket route now searches and groups what it got. That
leaves the domain with no service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The v2 spec families all live under models/ beside their row -- creators,
purgers, upserters -- but the update specs had been landing under repositories/,
which put one half of a domain's write declarations in a different layer from
the other half.

The twelve specs built on the v2 DataUpdater move to models/; the resource policy
trio and the retention policy join the packages their rows already share, the way
their creators do. Their imports carried nothing from repositories/, so nothing
follows them across the layer boundary.

The remaining twenty-three stay where they are: they subclass UpdaterSpec from
repositories/base/, so moving them would make models/ import repositories/. They
belong under models/ once they are migrated to the v2 family, not before, and
both AGENTS.md files now say which of the two a file in each place is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Which family an action belongs to is a judgment that reads as arbitrary once it
is made -- the code shows the base class, not why that base was the right one.
Four packages had that judgment settled during review, so each now carries a
table of its processor fields against the entity type, shape and operation they
run as, and the reasoning behind the ones that surprise.

The four are the ones actually examined: error log, storage namespace, role
preset, notification. The rest keep no document rather than a guessed one.

Two of the tables record a gap instead of a decision -- error logs authorize
through permission rows that are not seeded yet, and a storage namespace names
its storage through a column that cannot say which table it points at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ribe

A domain's KNOWLEDGE.md states the entity type, shape and operation each
processor field runs as. Nothing makes that table wrong when a field moves to a
different factory -- it just quietly stops being true, and the next reader trusts
it because it reads like a fact rather than a snapshot.

The guardrail names the trigger: a composition change updates the table in the
same change, and a domain that gains one without a document writes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The object and VFS storage packages answer two REST surfaces, and the v1 one
declared auth_required while v2 declared superadmin_required. Reading that as two
audiences, the earlier commit opened the catalog reads to any authenticated
caller so the v1 surface would keep working.

That was the wrong reading. Registering storages, listing them, and handing out
presigned URLs against them are all operations only a super admin performs; the
v1 declaration was the stale half, not the authoritative one. The reads go back
behind the gate and the nine v1 routes now say the same thing the processors do.

Six tests asserted a regular user could read the registry. They assert the
refusal instead -- the behaviour they were written against was never decided, it
was inherited from a processor that gated nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A preset is a PromQL template and executing one runs that query against the
metrics backend, which reads across the cluster rather than anything the caller
owns. The routes had declared the reads and the execute as auth_required while
the processors gated execute on the super-admin role, so the surface promised
something the action layer refused.

The reads join the writes behind the gate and the five routes now say the same
thing. The access test asserts the refusal it should have asserted from the
start; nothing else in the suite would notice, since the filter and preview
cases all authenticate as an admin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three domains wire a lookup and two called the field resolve_by_name, which says
how the key happens to be shaped rather than what the field is. Storage
namespaces resolve on a pair, not a name, so the older name would not have
survived being copied there anyway.

Also fold the login client type admin processors back into the domain's own
class. Which gate an operation carries is a property of that operation, and a
second class states it twice -- once in its name and again in the factory that
wires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… reads

Both operations carried an action, a service method, a processor field, a
repository method and a db_source query, and nothing called any of them. The
search that sits beside each already answers the same question -- these took an
owner id where the search takes conditions -- so removing them costs no coverage.

Their tests go with them. What they verified was that the service copies row
fields into a data type, which is the conversion the repository should have done
before returning; the search path in the same domain already does it that way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Record what each entity type constant stands for: the ownership tree, the
per-entity fields and references, the public and global groupings, and the
foreign-key rule for assignments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The marker is backfilled only from the former main_access_key, so a user who
deleted a keypair can carry none. Ordering by the newest let a new keypair move
which one answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two GetMy actions ran on the gateless legacy processor and never reached the
registry, so the catalog uniqueness sweep could not see them. They now take the
scopes as input and run behind the scope gate; the my-policy route is the adapter
naming the caller's own scope.

The join each repository method carried moves into an OperationScope, which
leaves both domains with no service and the keypair domain with no repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An action is global or public; that only super-admins pass the global gate is a
separate fact the gate decides. Seventeen action names introduced on this branch
said admin, and none of them exist on main, so no audit history splits here.

Processor fields wired from the global_* and public_* factories now carry the
same prefix, which is what the new Naming section in services/AGENTS.md states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HyeockJinKim
HyeockJinKim force-pushed the BA-7297-passthrough-sweep branch from 9d1e292 to 3d819c1 Compare August 16, 2026 03:15
Co-authored-by: octodog <mu001@lablup.com>
@github-actions github-actions Bot added size:XL 500~ LoC and removed size:L 100~500 LoC labels Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:common Related to Common component comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants