Changelog
Release history, one entry per tagged release.
The entries below are CHANGELOG.md from the repository root, which the release process updates when a version is prepared. Work that has landed but is not yet in a tagged release appears there under Unreleased and is left out here. The releases page carries the published binaries for each tag, and Implementation status covers what is built today.
Releases
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.4.0 - 2026-09-05
This release tightens authorization on the layer surface and fixes the
control-plane JSON field names. Registering a layer that names a filesystem path
on the registry host now requires the tenant administrator role, ingest is
confined to the directory a layer's path resolves to, and GET /v1/layers
reports only what the calling identity may read. A patch that asserts an owner
or a visibility field is now applied or refused rather than silently discarded,
and an admin-defined layer's visibility can be narrowed as well as widened. The
control-plane records that reached the wire under their Go field names now carry
the snake_case names the specification fixes. The bundled web UI is replaced by
an application covering the domain browser, search, the artifact viewer, and the
layer panel, and the registry acts as the OAuth client for a browser sign-in
flow. Runtime signing keys are read from operator configuration rather than
registered over HTTP, which closes a path to identity forgery.
Several of these are backward-incompatible and land in this MINOR bump, which
the pre-1.0 policy permits. A client reading a layer, receiver, quota, or admin
response by its Go field name must be updated, a reverse-proxy rule or bookmark
naming the web UI's /ui/ path must be changed to /app/, a deployment running
injected-session-token must write its runtime keys to
PODIUM_RUNTIME_KEYS_PATH before the registry will start, and a deployment
where a non-administrator registers local layers must grant those callers the
tenant administrator role or move the layers to a network Git source. A
values.yaml for the Helm chart must be updated for the renamed and removed
keys named below.
Added
- Web UI and browser sign-in (§13.10, §6.3.4, §7.3.4): a registry started with
--web-uiserves an application covering the domain browser, search with the type, scope, and tag filters, the artifact viewer with the manifest body rendered as markdown and its merged frontmatter as a property table, and the layer panel with register, edit, reorder, reingest, unregister, and restore. It replaces the read-only page the flag mounted before, which covered the domain browser and a raw-text artifact view. The registry is the OAuth client for the browser:GET /v1/ui/auth/sign-instarts the authorization-code flow,GET /v1/ui/auth/callbackperforms the exchange server-side and returns the identity provider's token in anHttpOnly__Host-podium_sessioncookie,GET /v1/ui/auth/sign-outclears it, andGET /v1/ui/sessionreports the signed-in caller's posture. The cookie is a second accepted location for the credential §6.3.3 already defines, so the flow adds no credential kind and the registry stores no session state. A deployment that configures an identity provider registers its/v1/ui/auth/callbackURL as a redirect URI with that provider. §6.10 gainsauth.csrf_invalidfor a callback whose state does not match andauth.exchange_failedfor a code exchange the provider refuses. Writes on/v1/layersrequire the caller to own the target layer or to hold the §4.7.2 admin role, where any authenticated caller could previously patch, reingest, or unregister another caller's user-defined layer. git_provideron a layer (§7.3.1): a layer names the git provider whose signature scheme verifies its inbound webhook deliveries.POST /v1/layersandPOST|PUT /v1/layers/updateacceptgit_providerin the request body, and a layer declared in theregistry.yamllayers:block sets it undersource.git.git_provider, beside the existingforce_push_policy. The field was assigned nowhere before, so every inbound delivery was verified under GitHub's signature scheme and a GitLab or Bitbucket source could not be configured to deliver at all. A value that names no registered provider, and any value on alocalsource, are refused with400 registry.invalid_argumentnaming the field; an unregistered value in the declared block aborts the boot with an error naming the layer and the value. The declared key is authoritative for a declared layer, because the boot re-seeds every declared entry from the configuration, so a value set over HTTP on such a layer reverts at the next start. An empty value resolves togithubat the point of use, so a layer stored before this change verifies as it did and no migration is required. No CLI flag sets the field.layer_capabilitiesin the session posture read (§7.3.4):GET /v1/ui/sessionreports an object naming what the requesting caller may do on the §7.3.1 layer operations on this deployment. It carriesmanage_any_layer, a boolean reporting whether the deployment's layer endpoints admit this caller on the §4.7.2 admin arm, which is the arm that decides a write on a layer the caller does not own and every operation the local-source authorization rule governs. The object and its member are always present, and the member isfalsewherever the deployment determines no capability for the request. A registry started with no identity provider configured, or one started in public mode, admits every caller on that arm and reports the member astruethere. The value is a snapshot taken when the read was answered, so an operation a client offers on the strength of it can still be refused, and the §6.10 envelope the operation's own endpoint returns remains the authority.emailin the session posture read (§7.3.4):GET /v1/ui/sessionreports the requesting caller's own email as the configured identity provider recorded it. The key is present only where an email resolves and is non-empty, and absent otherwise, and it belongs to the caller that asked and to no other caller. The web UI's account cluster renders that email as the signed-in reader's identity and falls back to the subject where the read carries none, so a deployment whose subject is an opaque provider identifier no longer draws a UUID there. The cluster still appears only where the read reports a subject.- Helm chart templates and values (
deploy/helm/podium): the chart renders an Ingress fromingress.enabledandingress.host, a bundled Postgres StatefulSet and Service frompostgresql.enabled, and a PersistentVolumeClaim for the filesystem object store fromobjects.enabled.runtimeKeys.enabledwithruntimeKeys.secretNamemounts thePODIUM_RUNTIME_KEYS_PATHkey file for theinjected-session-tokenidentity provider. The deployment renders the S3 and filesystem object-store settings,PODIUM_OAUTH_GROUPS_CLAIM,PODIUM_IDP_GROUP_MAPPING,PODIUM_BOOTSTRAP_ADMINS, andPODIUM_DEFAULT_LAYER_VISIBILITYas environment variables, and acceptsnodeSelector,tolerations,affinity,podAnnotations,podLabels,extraEnv,service.annotations, and aRollingUpdatestrategy.config.publicModeandconfig.allowPublicBindtravel together, because the chart renders a0.0.0.0bind for the kubelet probes and public mode refuses a non-loopback bind on its own.
Changed
- Runtime signing keys come from operator configuration (§6.3.2, §13.12):
POSTandGET /v1/admin/runtimeare removed and answer404. That endpoint registered a token-verification signing key under no authorization at all, and underinjected-session-tokenthe same key store backed the verifier, so an unauthenticated caller could install a key and then mint a token for any identity, a tenant administrator included. The registry reads its runtime public keys at startup from the file named byPODIUM_RUNTIME_KEYS_PATH. Underinjected-session-tokena path that is missing, unreadable, or carries no key aborts the boot withconfig.runtime_keys_unavailable.podium admin runtime registerwrites that file and requires--keys-file, and it calls no registry.podium admin runtime listis removed, because the target is a local JSON file. The §13.1.1 evaluation stack indocker-compose.ymlno longer selectsinjected-session-tokenand publishes its port on loopback. This is a backward-incompatible change and lands in a MINOR bump. No flag, environment variable, or configuration key restores the HTTP registration surface. - Re-embed authorization (§4.7, §4.7.2, §13.2.1):
POST /v1/admin/reembedrequires the per-tenant admin role and is refused on a read-only registry, which is what every sibling admin write already did. It previously checked the request method alone, so any authenticated caller could trigger a full-catalog re-embed, which bills the configured embedding provider and purges the tenant's vector rows that do not match the current model. A registry started with no identity provider configured, and one started in public mode, authenticate no caller and admit the call as before. - Visibility patches on an admin-defined layer (§7.3.1, §4.6, §8.1):
POST|PUT /v1/layers/updateapplies each visibility member the request body carries and preserves each member the body omits, so"public": falsewithdraws that axis,"organization": falsewithdraws that one, and"groups": []and"users": []empty those lists. The endpoint previously granted on each axis and withdrew on none: a body carrying a false boolean or an empty array left the stored value in place and answered200with a record reporting the visibility the patch tried to replace, so an admin-defined layer's visibility could be widened and never narrowed. JSONnullon a member is that member's empty value and withdraws on the same terms as[], and an emptied list is stored as absent, so the layer object reports it asnull.ownerkeeps its value reading: a non-empty value replaces the stored owner and an empty one leaves it. A stored record that sets no visibility field matches no §4.6 condition and reaches no composed view; it remains readable throughGET /v1/layersby a caller holding the §4.7.2 admin role, and a registry started with no identity provider configured, one started in public mode, and one started with no request-time identity verifier admit every caller as before. A layer declared in theregistry.yamllayers:block is re-seeded from that block at every start, so a narrowing applied to a declared layer over HTTP reverts at the next start, on the same terms asgit_provider.podium layer updatebuilds its request body from the flags the operator set rather than from the flags holding a non-zero value, so--public=falseand--organization=falseexpress a withdrawal where the value was previously dropped;--clear-groupsand--clear-usersempty their member, which the repeatable--groupand--userflags cannot express, and each is refused with exit 2 when combined with its repeatable flag. The web UI's Edit dialog offers a checkbox for each visibility axis and a removable token per stored group and user on an admin-defined layer, where it previously stated them as fixed. A patch that would store what the layer already holds writes no record, emits no §8.1 audit event, and wakes no watcher, and it answers200with the unchanged layer object as before.POST /v1/layers/reordercompares the tenant's precedence sequence before and after the write, so a reorder naming layers in the order they already hold emits nothing; it previously emitted on any non-empty order. A webhook-secret rotation, aforce_push_policychange, and agit_providerchange record theirlayer.config_changedaudit event and wake nopodium sync --watchsubscriber, because none of them can alter what a profile resolves. This is a backward-incompatible change and lands in a MINOR bump. No flag, environment variable, or configuration key restores the grant-only application or the unconditional emission. - Control-plane JSON field names (§7.2.1, §7.3.1, §7.3.2): the control-plane response records that reached the wire under their Go field names now carry the lower snake_case names §7.2.1 fixes. The layer object, returned by
POST /v1/layers,POST|PUT /v1/layers/update,GET /v1/layers,GET /v1/layers?deleted=true, andPOST /v1/layers/reorder, renamesIDtoid,SourceTypetosource_type,Repotorepo,Reftoref,Roottoroot,LocalPathtolocal_path,Ordertoorder,UserDefinedtouser_defined,Ownertoowner,Publictopublic,Organizationtoorganization,Groupstogroups,Userstousers,GitProvidertogit_provider,LastIngestedReftolast_ingested_ref,CreatedAttocreated_at, andDeletedAttodeleted_at.force_push_policyandlast_ingested_atalready carried snake_case names and are unchanged. The receiver object, returned byGETandPOST /v1/webhooksand byGETandPUT /v1/webhooks/{id}, renamesIDtoid,URLtourl,Secrettosecret,EventFiltertoevent_filter,Disabledtodisabled,FailureCounttofailure_count,LastDeliverytolast_delivery,LastFailuretolast_failure,CreatedAttocreated_at, andDebouncetodebounce. That rename is a projection over the stored receiver, so the operator'sPODIUM_WEBHOOK_STORE_PATHfile keeps its format and an existing file is read as it was.GET /v1/quotareports the limits underlimitsunder the namesGET /v1/admin/tenantsalready reported, renamingStorageBytestostorage_bytes,SearchQPStosearch_qps,MaterializeRatetomaterialize_rate,AuditVolumePerDaytoaudit_volume_per_day, andMaxUserLayerstomax_user_layers. Each element ofGET /v1/admin/show-effective'slayersarray renamesLayerIDtolayer_id,Visibletovisible, andReasontoreason. The bulk arm ofPOST /v1/admin/reembedrenamesTotaltototal,Succeededtosucceeded, andFailedtofailed, and each failure entry renamesArtifactIDtoartifact_id,Versiontoversion, andReasontoreason; the endpoint's single-artifact arm was already lowercase and is unchanged. The layer object no longer carries the tenant identifier, which was the registry's own stored tenant repeated on every record of a read that is not admin-gated, and the receiver object no longer carries itsTenantIDfor the same reason.GET /v1/quota's top-leveltenant_idand theidof each element ofGET /v1/admin/tenantsname the tenant that is the record's own subject and are unchanged. The receiver object'sdebounceis now the duration string the request body accepts, so a receiver created with"debounce": "60s"reads back as"1m0s"and that value can be sent toPUT /v1/webhooks/{id}without conversion, where the member was a nanosecond integer no request accepts; it is omitted on a receiver that sets no window. A read is not replayed whole, becausesecretis reported masked as***and the update stores whatever secret the body names, so a replayed read would overwrite the receiver's secret with the mask. The receiver object'screated_at,last_delivery, andlast_failureare emitted in UTC, where a registry process running in a non-UTC zone emitted them with that zone's offset. The layer object'screated_atis emitted in UTC on a Postgres deployment whose session time zone is not UTC, where the driver returned it in the session zone and the standard and standalone deployments reported the same field in two forms. This is a backward-incompatible change and lands in a MINOR bump. No flag, environment variable, configuration key, or content negotiation restores the Go-cased keys. A client reading any of these bodies by its Go field name must be updated to the name above. - Admin-only registration fields (§7.3.1):
POST /v1/layersrefuses a registration that assertsowner,public,organization,groups, orusersfrom a caller the §4.7.2 admin arm does not admit, with403 auth.forbiddencarryingdetails.constraint: "admin_only_fields"and a message naming the asserted fields. Such a registration previously discarded the assertion, stored the layer as user-defined with the implicitusers: [<registrant>]visibility, and answered201. A field is asserted by its value rather than by its presence:publicororganizationset to true, a non-emptygroupsorusers, and anownernaming a subject other than the caller's own. A false boolean, an empty array, an empty string, and anownernaming the caller's own subject assert nothing, so a registration that asserts none of the fields is unaffected and still answers201. The rule keys on the caller's admin arm rather than on the resolved layer class, so it reaches a re-registration of a stored layer the caller owns on the same terms, and a registry started with no identity provider configured, or one started in public mode, admits every caller on that arm and refuses nothing. Where a registration is on both this rule's arm and the local-source authorization rule's arm, the local-source refusal is the one returned. Operators should note that automation registering a layer as a non-admin with--public,--organization,--group,--user, or--user-defined --ownernaming another subject exited0on a registration that applied none of it and now fails with a non-zero exit and that envelope. The registration succeeds once the caller holds the tenantadminrole, or once the flag is dropped. This is a backward-incompatible change and lands in a MINOR bump. No flag, configuration key, or environment variable restores the discard. - Local-source layer authorization (§7.3.1): registering a layer that names a filesystem path on the registry host, patching a stored layer's filesystem path, restoring such a layer, and reingesting one now require the tenant
adminrole. Any other caller is refused with403 auth.forbiddencarryingdetails.constraint: "local_source", and the refusal names no filesystem path. Agitsource whose repository string resolves to go-git's file transport names a repository path on the registry host, so it is on the same arm and a non-admin registering one is refused; a repository string naming a network endpoint is not. The classification fails closed: a repository string the registry cannot place as a network endpoint is treated as naming a host path and is on the same arm, so a storedgitlayer whose repository string go-git rejects is refused for a non-admin on register, restore, reingest, and every inbound webhook delivery until an admin takes it or its repository string is corrected to a network endpoint. Agitsource is classified on its repository string alone, so a filesystem path stored beside one places nothing on the arm.unregisterandreordername no path and are unaffected. An inbound webhook delivery triggers a reingest and takes the same arm, so on a registry that authenticates its callers a delivery to agitlayer whose repository string names a path on the registry host is refused. A delivery to alocallayer is rejected earlier, with400 registry.invalid_argument, because the webhook path accepts agitsource alone. A registry started with no identity provider configured, or one started in public mode, authenticates no caller, so no caller holds the admin role and every operation the rule governs is admitted there as before. The default is closed: a deployment where a non-admin registerslocallayers today must grant those callers the tenantadminrole (PODIUM_BOOTSTRAP_ADMINSor a per-tenant grant), or move those layers to agitsource over a network transport, or have an admin register and reingest them. This is a backward-incompatible change and lands in a MINOR bump. No flag, configuration key, or environment variable restores the prior behavior. - Layer panel controls (§13.10): the web UI renders a control that would take a §7.3.1 layer write only where the caller's reported capabilities and the target layer's own fields admit it. A control the prediction refuses is absent rather than disabled, and a control it admits is still disabled by the read-only marker as before. The reordering affordance is the exception: a move names every layer in the moved row's class block and the registry refuses the request whole, so the handles stay present and disabled and the label names the reason,
Precedence — reordering these layers requires the administrator role. The registry remains the authority, so a refusal the panel could not predict is still reported as the endpoint's error envelope. - A set of accepted audiences (§6.3.3):
PODIUM_OAUTH_AUDIENCEand the config-file keyidentity_provider.audienceconfigure a set of audience values the registry answers to rather than a single value. The environment variable takes a comma-separated list, and the config-file key takes a string or a list of strings; a string is one audience verbatim and is not split on any separator. Entries are trimmed, blank entries are dropped, and repeated entries are collapsed keeping the first occurrence. A token is accepted when itsaudclaim carries at least one configured value, underoidc-jwtand underinjected-session-tokenalike, and a caller admitted under any configured value has the same effective view, the same grants, and the same audit identity. A token carrying noaud, an emptyaudlist, or anaudthat is the empty string is still rejected, and a setting that resolves to no entry still fails startup withconfig.oidc_jwt_audience_unsetorconfig.injected_token_audience_unset. The first configured value is canonical and is the audience the §6.3.4 browser sign-in redirect asks the identity provider for, so a deployment enabling that flow lists the value its browser client is issued first. Theoidc-jwtstartup log now names the accepted audiences beside the accepted issuers.podium config show --serverreports the set joined with commas underoauth_audienceand attributes the row toPODIUM_OAUTH_AUDIENCEwhen the environment variable set it and toregistry.yamlotherwise, where it previously reporteddefault. A registry configured with one audience behaves as it did. --localusage strings: the--localflag help onpodium layer registerandpodium layer updatenames the administrator role the registry now requires for a filesystem path.- Helm chart defaults (
deploy/helm/podium): three defaults each stopped ahelm installon their own.config.identityProvider.typedefaulted tooauth-device-code, which the registry refuses at startup withconfig.identity_provider_unverified, and is nowoidc-jwt.config.embeddingProvider.typedefaulted toopenai, which exits withoutOPENAI_API_KEY, and is nownonewith hybrid search as an opt-in.config.bindwas declared and rendered by no template, so the registry kept its loopback bind and the kubelet probes could not reach the pod; it is rendered asPODIUM_BIND.config.identityProvider.authorizationEndpointis renamed toconfig.identityProvider.issuer, which is the keyoidc-jwtreads, andconfig.identityProvider.audiencedefaults to empty so a value supplied throughexistingSecretis not overridden by a placeholder. Theconfig.discovery.*values were reachable by no route, because §13.12 discovery is config-file-only and this chart renders no ConfigMap, and the inertingress:andserviceMonitor:value blocks are removed.envFromrenders only whenexistingSecretnames a secret, where an empty value previously produced a manifest the API server rejects. Avalues.yamlnaming the removed or renamed keys must be updated. - Web UI path: the bundled web UI is served at
/app/instead of/ui/, and a registry started with--web-uiredirectsGET /to it./ui/is no longer served and no alias replaces it, so a reverse-proxy rule or a bookmark naming the old path must be updated. The browser-flow routes are unchanged:/v1/ui/auth/sign-in,/v1/ui/auth/callback,/v1/ui/auth/sign-out, and/v1/ui/sessionkeep their paths, so no identity-provider client configuration and no registered redirect URI changes. A registry started without--web-uianswersGET /exactly as before.
Fixed
- Owner and visibility patches on a user-defined layer (§7.3.1, §4.6):
POST|PUT /v1/layers/updaterefuses a patch that assertsowner,public,organization,groups, orusersagainst a stored user-defined layer, with400 registry.invalid_argumentcarryingdetails.constraint: "immutable_visibility"and a message naming the asserted fields. Such a patch previously discarded the assertion, left the stored record's owner and visibility unchanged, and answered200, so a caller was told a widening applied that never did. A field is asserted by the value the patch would store rather than by its presence:publicororganizationset to true, a non-emptygroups, ausersdiffering from the layer's storedusers, and anownernaming a subject other than the layer's stored owner each assert the field. A member asserts when the value the patch would store differs from the stored one, so an absent member, a false boolean, an emptygroups, an empty string, and a restatement assert nothing while emptying a storedusersasserts and is refused, and a client that reads a layer object and sends it back verbatim is still admitted; the layer object carriesowneranduserson every layer, and a stored user-defined layer holdsusers: [<owner>]. The comparison against a stored value is exact, element for element and byte for byte, so a value differing only in element order or in surrounding whitespace asserts the field. The rule reads the stored layer's class rather than the requesting caller, so it binds every caller the layer write rule authorizes, a tenant admin included, and a registry started with no identity provider configured and one started in public mode refuse on the same terms. The refusal rejects the whole request: arotate_webhook_secretcarried in the same body mints no secret, no other field the same body carries is applied, no record is written, and no §8.1 audit event is emitted. A patch the layer write rule or the local-source rule refuses keeps its ownauth.forbiddenenvelope, because this rule is evaluated after both. An administrator who needs the layer visible more widely re-registers its ID throughPOST /v1/layersas an admin-defined layer carrying the visibility they declare; that re-registration replaces the stored record, so the registration time becomes the time of the re-registration, the order is recomputed at the tail of the layer list,last_ingested_refandlast_ingested_atare emptied, agitsource is issued a new inbound webhook secret that must be registered at the Git host, and the former owner regains a slot against the per-identity user-defined layer cap. A request that received200before receives400after, which reachespodium layer update --owner,--group, and--user, and--publicand--organizationwhere the flag is set true, against a user-defined layer, and an operator on a deployment with no identity provider who corrected an--ownertypo that way now reads the refusal and re-registers instead. This is a backward-incompatible change and lands in a MINOR bump. No flag, environment variable, or configuration key restores the discard. - Unconfined local layer path: no authorization governed the filesystem path a layer named, so an authenticated non-admin could register a layer pointing at any directory readable by the registry process and have its contents ingested and served back. The local-source authorization rule above closes that path, and the ingest confinement below bounds what a layer that is admitted may read.
- Ingest reads through a symbolic link leaving the layer root (§7.3.1): an ingest that reads a layer's configured filesystem path as a directory now reads only within the directory that path resolves to, whichever caller declared the layer and in every deployment mode, including a layer the operator declares in the registry's own configuration. The confinement is not configurable. A relative symbolic link that resolves within the directory is read. A symbolic link whose target is written as an absolute path is refused whatever it resolves to, including a target inside the same layer, and such a link is repaired by rewriting it with a relative target. A read the ingest requires and cannot satisfy fails that layer's whole ingest, which reports
ingest.source_unreachable, so no artifact and no bundled resource from that snapshot is accepted while the artifacts served before the refusal stay in place until the layer is restructured. ADOMAIN.mdthe refused cycle read before the failing read is still committed, and it emits itsdomain.publishedevent only where that file was added or changed since the previous ingest, so a cycle the confinement refuses repeatedly over an unchanged domain emits no further event. - Layer list visibility:
GET /v1/layersreports what the calling identity may read, on both the live and the?deleted=truearm, and the reorder response reports the same set. A caller holding the tenantadminrole still receives the tenant's whole layer list. Any other authenticated caller receives the layers visible to that identity. A caller the registry resolves as anonymous receives an empty list, and a caller whose credential fails verification is refused with the sameauth.token_expired,auth.untrusted_token, orauth.untrusted_runtimeenvelope the registry's other routes already answer for that credential. A layer outside that set is absent from the response rather than refused, and no error code reports the narrowing. A registry started with no identity provider configured, and one started in public mode, return the whole layer list to every caller as before. On upgrade, a signed-in non-admin sees fewer rows inpodium layer listand in the web UI layer panel, and an unauthenticated caller against a registry that configures an identity provider sees none. A caller presenting a stale or forged token toGET /v1/layersnow receives that refusal where it previously received the full list, and the same caller receives it fromPOST /v1/layers/reorderin place of the403 auth.forbiddenthe write gate answered before, because the registry now reports that it could not verify the credential before it evaluates whether that caller may write. - Search over an
extends:child: an artifact that inheriteddescription,tags,sensitivity, orsearch_visibilityfrom its parent was indexed and filtered under its own authored values, sosearch_artifactscould not find it by the descriptionload_artifactserved for it. Ingest now folds the pinned parent into those four indexed columns, and a search result carries the resolved sensitivity. - Parent disclosure in a search result: the
frontmatterblock of asearch_artifactsresult carried the child'sextends:line, which names a parent the caller may not be able to see. The key is now removed before the block is returned, and the block is empty when the stored frontmatter cannot be decoded. - Public-mode sensitivity floor over an inherited value: a registry in public mode evaluated the floor against the sensitivity an artifact declared itself. An artifact declaring
extends:and nosensitivity:was admitted and then stored at the parent's level, which is the level the floor refuses. The floor is now evaluated again on the inherited value, and such an artifact is rejected withingest.public_mode_rejects_sensitive. podium sync --watchagainst an authenticating registry: the watcher built its/v1/eventssubscription with noAuthorizationheader while the one-shot fetch attached the caller's token, and/v1/eventsis not exempt from identity verification, so a registry configuring an identity provider answered401and the watcher reconnected every 500ms without ever triggering a run. The non-2xx response returned silently into the reconnect loop, which made a rejected subscription indistinguishable from an idle one. The subscription carries the caller's bearer token, and a non-2xx subscription response logs its status.layer.config_changedreached no subscriber (§7.5.4):podium sync --watchsubscribes toartifact.published,artifact.deprecated, andlayer.config_changed, and nothing published the third. The layer endpoint recorded the §8.1 audit event on register, unregister, and reorder and stopped there, so an administrative layer change left every watcher serving a stale profile until an unrelated artifact event happened to wake it. Those operations publish the event, which reaches the/v1/eventsstream a watcher holds and any §7.3.2 outbound webhook receiver whose filter matches. A personal layer emitslayer.user_registeredand is excluded, because waking every watcher in the tenant for one caller's own composition resolves nothing.- Startup guard recommendation: the guard that refuses a registry whose identity provider has no request-time verifier told the operator that only
injected-session-tokenis verified server-side.oidc-jwtandtrusted-headershave request-time verifiers, and the message pointed at the one provider that requires a runtime signing key before it verifies anything, so an operator fronting the registry with a gateway was sent to build a runtime-key onboarding flow instead of settingtrusted-headers. The message is built from the boot path's own list of verified providers. - Hook subtype events materialized without a tool matcher (§4.3.5): an artifact declaring a tool-category hook event such as
pre_shell_executionmaterialized on a harness that emits only the generic tool event as a bare generic entry with no tool-name matcher, so its action ran ahead of every tool call rather than the category the author named. A formatter declared for file edits ran on shell and MCP calls, and the action succeeded each time, so nothing reported the misfire. The Claude Code, Codex, and Gemini adapters write the harness's own tool-name matcher beside the generic native event:^Bash$for shell,^mcp__or^mcp_for MCP, and on Claude Code^Read$,^(Edit|Write|NotebookEdit)$, and^Task$for the read, edit, and subagent categories. A generic event, a permission-category event, andpost_tool_use_failurename no tool category and stay unnarrowed. Cursor emits the subtypes natively and is unchanged. Materialized output for an affected hook changes at the nextpodium sync. - Signature and body on an
extends:child: the merged record anextends:child was served carried the child's content hash beside the root parent's signature envelope, so verification compared a signature over one hash against a different hash and could never succeed. Materialization refuses a signed artifact whose signature fails, which made signing andextends:mutually exclusive with no way to author around it. The child's own signature is carried. The body was carried only where the child authored a non-empty one, so a child with no prose was served the root parent's, which reaches a requester who may hold no access to the parent's layer; the body is carried unconditionally, matchingmanifest.MergeExtendsand the filesystem resolver. - Undeclared frontmatter keys through an
extends:merge (§4.6): both extends resolvers serialized the merged manifest by marshalling a closed struct, which dropped every key it does not declare, including the extension-type fields §4.6 names among the keys a child inherits. Serialization runs through one implementation that the server and filesystem resolvers both call, so the two deployment modes serve the same bytes for the same artifact. Restored keys are authored text, so a child whoseextendsreference arrives through a YAML merge key presents an operativeextendsvalue in the reassembled block and is refused withregistry.invalid_argument, where the closed round-trip previously dropped that mapping and served a clean block. Comments on restored nodes are cleared before the block is served, because a comment on a parent's key can name the parent to a requester who cannot see the parent's layer. - Inherited
audit_redacton the read event (§8.2): the §8.2 emitter derived its redaction key set from the stored leaf record, so a child that inherits its parent'saudit_redactdirective carried no keys of its own and the directive named nothing. The emitter reads the record the caller was served, with the folded directive and the value source it names. - An
extends:pin onto a deprecated parent (§4.6, §4.7.6): a range or unpinnedextends:reference filters deprecated versions out of the candidate set, so a re-ingest re-pins onto the live version rather than onto a parent deprecated after the child was first ingested. An exact or content-hash pin naming a deprecated version is refused withingest.invalid_artifact, because the author named that version. A reference whose every stored candidate version is deprecated is refused with a message naming deprecation, so the report does not state that the parent was never published. A child that inheriteddeprecated: truewas reported deprecated byload_artifactand reported live bysearch_artifacts,podium sync, and impact analysis, and closing the ingest path removes the state those readers disagreed on. The same change correctslatestin the resolver to the most recently ingested non-deprecated version, which is the definition §4.7.6 fixes and which the read path already used, where the resolver previously selected the highest semantic version and read no deprecation flag. An existing child is not repaired, because(tenant, artifact_id, version)is immutable, and republishing the child at a new version takes the corrected resolution. - Artifact description on the Frontmatter tab (§13.10): the artifact viewer's header dropped its description paragraph when the Frontmatter tab was open and the manifest declared the field, so switching tabs made the sentence vanish and the page reflow around it. The header stands on every tab. The top bar's appearance control is drawn as its icon alone and carries an accessible name in place of the visible label.
- Embedding projection drift: ingest and
podium admin reembedcomposed the embedding text differently for a record holding an emptywhen_to_useortagsentry. Both now use one implementation, which drops the empty entry.
Operators upgrading should note that a version already ingested keeps its unfolded description, tags, sensitivity, and search_visibility. Re-ingesting unchanged bytes is counted idempotent and skipped before the write, and podium admin reembed reads the stored columns, so neither repairs an existing row. The repair is publishing a new version of the child, which is ingested as a new row and takes the fold. A child of a direct-only parent stops being indexed once republished, which brings search into agreement with what layer resolution and load_artifact already report for that artifact.
Documentation
- Corrected the specification, the HTTP API reference, and the operator runbook where they offered
oauth-device-codeas a registry-process identity provider. It is client-side acquisition, the registry ships no request-time verifier for it, and a registry configured with it refuses startup withconfig.identity_provider_unverified. Theregistry.yamlexample in §13 selectsoidc-jwtand namesissuerrather thanauthorization_endpoint, the read-only write set in §13.2.1 drops the claim that the registry issues tokens against a local session table, and the web-UI paragraph stops presenting the device-code flow as an authentication mode of a standard deployment.
0.3.1 - 2026-08-18
The documentation site is now built by a generator in this repository rather than by Jekyll, and the documentation it publishes was corrected against the code and the specification.
Changed
- Documentation site: the published site is generated from the markdown under
docs/by the package insite/, replacing the Jekyll and Just-the-Docs build. Frontmatter is a closed key set, and the build gate rejects an unresolvable link, a link to a missing anchor, an unknown code-fence language, an image with no alt text, a heading outline that skips a level, and a text token that misses the WCAG AA contrast ratio on the surface it renders on. Every page is complete HTML, and the browser bundle adds client navigation over it. - Changelog page: the site publishes this file directly, through an
include:key in the page's frontmatter, so the page follows the file the release process edits.
Fixed
- Evaluation stack startup:
docker-compose.ymlselected the Postgres store and left the embedding configuration unset. A Postgres-backed registry defaults its embedding provider toopenai, which then requiresOPENAI_API_KEY, so the registry exited at boot withmissing required configuration for the selected backend(s): OPENAI_API_KEY. The stack now setsPODIUM_NO_EMBEDDINGSand comes up withdocker compose up -dalone, serving keyword search. Remove that variable and supply a provider with its key to exercise hybrid search.
Documentation
- Corrected the documentation against the code across two audits, each finding independently confirmed before it was applied. Among them: the default
SignatureProviderisnooprather than Sigstore-keyless; no notification provider is wired unless the operator names one;registry.yamlis read fromPODIUM_CONFIG_FILEor$HOME/.podium/, never from/etc/podium/; model-versioned vector rows and the stale-row purge are a capability only the collocated backends implement; a resource exactly at the inline cutoff travels inline; and anextendspin takesmajor.minor.patchormajor.minor.x, so the@1.2examples could not resolve. - Resolved contradictions between pages in different sections: SCIM push, freeze windows, and the hash-chained audit log are available on a single node;
podium loginis a no-op only for a filesystem registry or a loopback default;podium syncperforms no signature or content-hash verification;mcp-serveris the built-in extension type rather than a first-class type; and public mode enforces a sensitivity floor at ingest. - Redrew the
podium sync --watchdiagrams. Both described a per-event incremental pipeline that does not exist: the watcher reads only the event type from a newline-delimited JSON stream, holds one debounce timer, and reruns the whole sync. - Added the mobile layout for the site, at the breakpoints the design defines.
0.3.0 - 2026-08-15
AD FS compatibility for the oidc-jwt identity provider: the registry accepts the discovery document's access_token_issuer as a second token issuer, reads the subject and group claims under operator-named claim names, and reads a group claim emitted as a single string.
Added
- Second accepted token issuer under
oidc-jwt(§6.3.3): the registry accepts a forwarded token whoseissmatches the configuredidentity_provider.issueror theaccess_token_issuerthe same discovery document publishes. The second value is read once, when that document resolves, and it is compared as a string and never dereferenced, so the signing keys still come from thejwks_uriin the configured issuer'shttpsdocument. A document that publishes noaccess_token_issuerleaves the configured issuer as the sole accepted value. When the two values differ, the startup log names both. AD FS is the deployment this rule covers: it serves discovery underhttps://<host>/adfsand stamps the federation-service identifierhttp://<host>/adfs/services/truston the access token. PODIUM_OAUTH_SUBJECT_CLAIMandPODIUM_OAUTH_GROUPS_CLAIM(§6.3.3, §13.12): the config-file keysidentity_provider.subject_claimandidentity_provider.groups_claimname the claim the registry reads as the caller's subject and the claim it reads for group membership. Whensubject_claimis set the registry reads that claim alone and rejects a token that does not carry it withauth.untrusted_token, with no fallback tosub. Both settings are unset by default and are read only underoidc-jwt. The recorded subject keysusers:layer visibility, user-defined layer ownership, per-tenant admin grants, and the instance-operator grant, so a deployment that setssubject_claimlists values of the named claim inPODIUM_OPERATOR_ADMINSandPODIUM_BOOTSTRAP_ADMINS.
Changed
- Group-claim encoding (§6.3.1): the
oidc-jwtandinjected-session-tokenverifiers both read a group claim in the multi-value form and in the single-string form an IdP emits for a caller in exactly one group. The single-string form yields one group whose name is the whole claim value, and it is not split on any separator. A deployment whose IdP emits that form previously resolved to an empty group list without an authentication error, so group-scoped layers that were invisible to such a caller become visible on upgrade.
Fixed
- Keychain entries larger than the backend limit: the macOS keychain backend rejects a payload over roughly 3000 bytes, and an AD FS refresh token runs to about 4.7 KB, so
podium loginfailed at the save step. The credential store now splits a token larger than 2500 bytes across numbered entries and records the chunk count in a marker under the bare label, reassembles the chunks on load, and clears the previous save's entries before a re-save so a laterpodium logoutremoves the whole token. ownerin the Claude and Cursor marketplace manifests (§7.8): the rootmarketplace.jsoncarried onlynameandplugins, and the Claude schema requiresowner.name, so Claude Desktop refused to import a Podium-published marketplace repository. The Claude and Cursor manifests now emitowner.nameset to the marketplace name. The Codex manifest, whose format documents noowner, is byte-identical to the released output.
0.2.1 - 2026-06-30
Webhook hardening: the outbound webhook receiver endpoints are admin-gated, receiver URLs are validated against an SSRF policy, and a receiver can coalesce a burst of events into one batched delivery.
Added
- Receiver SSRF policy (§7.3.2): the registry validates a webhook receiver URL at registration and re-checks it at delivery. By default it requires the
httpsscheme and rejects a URL that resolves to a loopback, link-local, or private address, and it does not follow a redirect to such a target.PODIUM_WEBHOOK_ALLOWED_TARGETS(§13.12) allowlists hosts or CIDRs for a legitimately-internal receiver and overrides the address rejection, not thehttpsrequirement. A rejected target returnsregistry.invalid_argumentnaming the disallowed host. - Per-receiver debounce window (§7.3.2): a receiver with
debounceset coalesces the events it matches in a trailing window into one batch delivery, deduplicated by event type and key, sent with the same retry, backoff, concurrency limit, and HMAC signing as a single-event delivery. The batch envelope is additive; the single-event body is unchanged for a receiver without a window.
Changed
- Receiver authorization (§7.3.2): the
/v1/webhooksreceiver CRUD endpoints (GET,POST,PUT, andDELETE) require the per-tenant admin role and returnauth.forbiddenfor a non-admin caller, alongside the existing read-only rejection for the mutating methods. This closes the gap where any authenticated caller, or an unauthenticated standalone bind, could register a receiver and point the registry at an internal endpoint.
0.2.0 - 2026-06-29
Marketplace publishing: a podium sync target of kind: marketplace renders the catalog into a harness-native git-repo marketplace and runs an operator-configured workflow to push it to a remote.
Added
- Marketplace publishing (§7.5.2, §7.8): a
podium synctarget ofkind: marketplacerenders the effective view into a harness-native git-repo distribution and runs an operator-configuredworkflowof shell commands to clone, commit, and push it to a git remote. One repository carries the Claude (Code, Desktop, Cowork), Codex, and Cursor plugin-marketplace manifests at their fixed locations, while Gemini (extension), Pi (package), and Hermes (tap) take their own repository. Theplugins:list groups artifacts by scope filter, and the publishingidentity:governs the visibility-filtered effective view that reaches the marketplace. Podium renders to a folder and never holds a git push credential.podium sync --configruns the prepare, render, and publish pipeline per target, and--checkand--dry-runwrite nothing. - The
HarnessAdapterSourcecarries a plugin descriptor, so an adapter can render an artifact into a named plugin (§6.7, §9.1).
Changed
claude-coworkis publish-only (§6.7, §6.7.1): the cowork adapter no longer materializes the plugin-layout artifact types (skill, agent, command, rule, hook, and mcp-server) throughpodium sync; they reach Claude Cowork through akind: marketplacemarketplace instead. Atype: contextartifact still materializes to.podium/context/underpodium sync. The §6.7.1 capability cells forclaude-coworkare regraded to unsupported for the affected rows.podium syncenforces the §6.9 untranslatable rule: a target whose harness cannot represent a selected artifact fails rather than silently skipping it, matchingload_artifact.
Documentation
- Added a marketplace-publishing guide and a publish-flow diagram, and reframed the harness, CLI, and error-code references onto the
kind: marketplacesync target.
0.1.6 - 2026-06-17
The podium-mcp stdio server returns a spec-compliant MCP CallToolResult, so hosts that render result.content show meta-tool output instead of an empty result.
Fixed
- MCP
tools/callresult format (§6.1.1):podium-mcpreturns each meta-tool result as an MCPCallToolResult. The domain object is carried instructuredContentand mirrored as acontent[]text block, and a §6.10 error envelope setsisError. Hosts that renderresult.content(Claude Code, Claude Desktop, Cursor, and VS Code) previously received no content and showed an empty result forsearch_artifacts; they now display the output. The meta-tool fields move from the result top level tostructuredContent.
Documentation
- Documented the
tools/callresult format in §6.1.1, and corrected the §5load_artifactdescription so it states materialization writes the adapted body as a harness-native file in addition to any bundled resources.
0.1.5 - 2026-06-10
A standalone server pointed at a filesystem registry now honors per-layer .layer-config visibility at boot, instead of stamping one deployment default on every layer.
Fixed
- Standalone bootstrap (§4.6, §13.11.1): a
PODIUM_LAYER_PATHfilesystem registry served by a standalone server applies each layer's declared.layer-configvisibility. A layer that declares a non-empty visibility boots with it; a layer with no.layer-config, or one whosevisibility:block is empty, falls back to the deployment default (PODIUM_DEFAULT_LAYER_VISIBILITY), matching how a declarativelayers:entry resolves an empty block.
Documentation
- Documented the optional per-layer
.layer-configfile and itsvisibility:schema in the filesystem-registry directory layout (§13.11.1) and the solo/filesystem deployment guide.
0.1.4 - 2026-06-08
Multi-tenancy and gateway-delegated authentication. Two design proposals land: server-side request authentication for a registry behind an identity-aware gateway (proposal 0001), and runtime tenant provisioning through an operator-authorized API and CLI (proposal 0002). The boot-time PODIUM_TENANTS environment variable is replaced by the runtime provisioning path.
Added
- Server-side request authentication (§6.3.3, proposal 0001): the
oidc-jwtandtrusted-headersidentity providers authenticate each caller from a gateway-forwarded token or trusted request headers, selected byPODIUM_IDENTITY_PROVIDER. The caller's organization comes from the verifiedorg_idclaim or theX-Podium-User-Orgheader. - Per-request multi-tenant routing (§6.3.1): a registry started with
PODIUM_MULTI_TENANTresolves each request to the tenant its organization names, and rejects an organization that names no provisioned tenant withauth.tenant_unknown. A single-tenant registry binds every request to its sole tenant and does not consult the organization value. - Runtime tenant provisioning (§7.3.3, proposal 0002): the operator-authorized
/v1/admin/tenantsAPI and thepodium admin tenantCLI create, list, update, and deactivate tenants on a live multi-tenant registry. The instance-operator role is seeded withPODIUM_OPERATOR_ADMINSand is distinct from the per-tenantadminrole. Per-tenant quotas and the §3.5 scope-preview gate are set at create or update, and create is idempotent. Deactivation is soft: a deactivated tenant stops resolving while its data persists, and reactivation restores it.
Changed
podium domain analyzetakes the path as a positional argument (podium domain analyze <path>), matchingpodium domain showandpodium domain search.
Removed
- The boot-time
PODIUM_TENANTSenvironment variable and the boot-time tenant-provisioning path. A multi-tenant deployment seeds its first operator withPODIUM_OPERATOR_ADMINSand provisions tenants at runtime through the API or CLI. - The
lint.hook_generic_and_subtypelint rule, which rejected a hook that declared both a generic tool-call event and a subtype event. The rule could not be enforced across independently authored layers, and declaring both events is a legitimate pattern.
Fixed
- SDKs (§7.2):
load_artifactcontent above the 256 KB inline cutoff on a single load is fetched from the presigned manifest-body URL instead of failing (podium-py,podium-ts). - Store (§4.7.1):
Memory.CreateTenantis idempotent, matching the SQLite and Postgres backends, so re-creating an existing tenant leaves the stored row unchanged. - Registry: graceful shutdown runs through a single server lifecycle context.
Documentation
- Clarified what
load_artifactreturns inline versus what materializes to disk, for the MCP server and the SDKs (§6.6, §6.7). - Corrected the CLI, HTTP API, error-code, and authoring references against the implementation.
0.1.3 - 2026-06-04
Spec-conformance and reliability release. The bulk of the work reconciles the implementation with the specification across the registry, CLI, MCP bridge, and SDKs, and builds out the test infrastructure that verifies it (live integration lanes for Postgres, S3, and the managed vector backends; spec, doc, and matrix coverage gates; and a hand- and agent-runnable end-to-end validation suite). The user-facing changes are grouped below by area; the internal test and CI work is omitted.
Added
- Managed vector backends: Pinecone, Weaviate Cloud, and Qdrant Cloud, alongside the existing
sqlite-vecandpgvector, with both externally-computed embeddings and backend-side integrated inference. - Observability (§13.8): an opt-in Prometheus
/metricsendpoint on the registry and the MCP bridge, and OpenTelemetry trace export with W3C context propagation. - Per-tenant daily audit-volume quota (§4.7.8) and reverse-dependency in-degree ranking in search (§4.7.3).
- Transactional vector outbox with a drain worker, and per-row embedding-model versioning with a mixed-model query restriction (§4.7, §4.7.2).
- Consumer-side
verify_signaturesdefault read fromsync.yamlfor standalone deployments (§13.10), and config-merge / managed-marker materialization ops (§6.7).
Changed
podium statusandpodium config showresolve the registry and harness from the mergedsync.yaml(the flag, then the environment, then the config), not only from environment variables;config showhints when no configuration is in scope and surfaces effective server settings under--server.- The MCP bridge negotiates down to an older MCP protocol version, rejects a filesystem-source registry, and refuses an incompatible client version (§6.1, §6.9).
Fixed
- Artifact model, ingest, and lint (§4.1–§4.4): the type system and sizing lint, canonical IDs and the resource boundary, manifest schema parsing, skill and hook ingest lint, prose artifact-reference resolution, document-source provenance, URL status checks, the seccomp baseline, DOMAIN.md body-size lint, and configurable bundled-resource caps; binary inline resources are base64-encoded and served without an object store.
- Domains (§4.5):
DOMAIN.mdcomposition is ingested and applied atload_domain, with discovery rendering, tenant config, and imports. - Layers, visibility, and versioning (§4.6, §4.7): extends-merge / collision / visibility composition, the per-identity user-defined layer cap, runtime layer resolution, embedding projection and version resolution,
replaced_byrecovery on load for the SQL backends, and extends-pinned-parent protection from deprecated-version purge. A same-IDextendsoverlay from a lower-precedence layer is no longer rejected as a self-extends cycle. - Meta-tools and MCP bridge (§5, §6): verbatim §5.1 tool descriptions and input schemas, the §6.6 materialization pipeline (content-hash verification, hook script path, rule fidelity), the §6.5 resolution cache (TTL, HEAD revalidation, prune safety), the §6.4 workspace overlay (watch / re-index, fused
total_matched), per-harness materialization targets (§6.7 — codex hooks intoconfig.toml, cowork buckets, config-merge ownership so gemini acceptsmcpServers), the §6.2 server config env vars, and the §6.10 structured error envelope. The content cache now persistsskill_rawand the sensitivity/signature envelope, fixing acontent_hash_mismatchand a skipped signature check on cache hits.search_artifactstotal_matchedcounts vector-only hits, and the hybrid BM25 half indexes only the §4.7 searchable projection (name, description, when_to_use, tags) with stopword filtering, so a paraphrased query ranks by vector similarity. - External integration and sync (§7): §7.2 bundled-resource delivery and the presigned manifest-body channel above the inline cutoff, §7.3 inbound webhook and reingest pipeline (
last_ingested_at,force_push_policy, break-glass, webhook-secret rotation and redaction), §7.4 degraded-network cache-mode fallback across the bridge / sync / SDKs, §7.5.2 sync honoringPODIUM_HARNESSwith profile / scope and lock provenance, §7.6 read CLI and SDK--jsonschemas and caller-credential propagation, and §7.7 onboarding (initwalk-up / wizard / hints, login resolution).cache prune --days 0is accepted as the "older than now" boundary. - Identity and scope preview (§6.3, §3.5): injected-session-token verification, device-code, scope and group mapping,
audenforcement, and token watch; scope-preview endpoint correctness and the tenant gate, surfaced instatus/sync/ MCP. - Audit and observability (§8, §12, §13.7, §13.9): registry audit events under dotted
caller.*keys, §8.2 PII redaction, §8.4 sampling / retention / re-anchor, §8.5 right-to-be-forgotten erasure (purge, redaction, tombstone, salt guard), §8.6 gap-detection scheduling, immutableCache-Controlon content-addressed reads, §13.9 health and readiness probes, and §12 offline status / ETag revalidation / learn-from-usage rerank. - Deployment and config (§13, §14): the §13.1.1 evaluation compose stack (registry, Dex, bootstrap-admin seeding), §13.2 read-only write rejection / public-mode bind guard / sensitivity ceiling / read-only probe and recovery, §13.4
migrate-to-standardshort-form flags and standalone-tenant resolution, §13.10 standalone zero-flag and first-run~/.podium/sync.yamlauto-bootstrap, §13.11 fsnotify watch and filesystemextends, and §14.9 / §14.10 enterprise-layer register-class inference andlayer watch --interval. - Retrieval and SPIs (§3.2, §3.3, §9): hybrid domain search with vector-only fusion, description-quality advisories with MCP session correlation, the §9.1 operational notification on ingest failure, context-first SPI signatures, and a structured SPI error envelope.
Security
- The
/objects/{content_hash}data-plane route was exempt from identity verification and served restricted bytes to any caller. Visibility is now enforced on that route, and S3 presigned URLs no longer embed credentials.
0.1.2 - 2026-05-11
Distribution-channel additions. No changes to the CLI surface; the binaries themselves are bit-identical to v0.1.1 (modulo the embedded version string).
Added
- Per-platform archives alongside the individual binaries on each GitHub Release:
podium-<os>-<arch>.tar.gz(Linux + macOS) andpodium-windows-amd64.zip, each containingpodium,podium-server, andpodium-mcpwith their canonical un-suffixed names. The individual binaries are still attached; the archives are additive for package-manager consumers. - Homebrew tap and Scoop bucket update job in
release.yml. On each tag push, the workflow patchesFormula/podium.rbinlennylabs/homebrew-tapandbucket/podium.jsoninlennylabs/scoop-bucketsobrew install podium/scoop install podiumtrack the latest release. Both auxiliary repos are org-wide — one repo per package manager, one file per Lenny Labs project. TAP_BUCKET_TOKENrepo secret requirement, documented in OPERATIONS.md.
0.1.1 - 2026-05-11
Release-pipeline fixes. The v0.1.0 tag was created but never produced published artifacts (PyPI, npm, GHCR) because of a sequence of CI configuration failures; v0.1.1 is the first version where the release workflow runs end-to-end. The behavior of the code itself is unchanged from what v0.1.0 was supposed to ship — see the 0.1.0 section below for the feature list.
Release-pipeline fixes since v0.1.0
- Container builder switched from alpine/musl to debian/glibc; sqlite-vec.c uses BSD type names that musl doesn't provide.
- Cross-compile matrix now uses CGO_ENABLED=1 with per-target toolchains (gcc on linux/amd64, gcc-aarch64-linux-gnu on linux/arm64, mingw on windows/amd64, native clang on darwin/arm64).
- Windows binary build moved to a windows-latest runner with a workflow step that fetches sqlite3.h from the SQLite amalgamation.
- npm package gains
repository/homepage/bugs/keywordsfields so npm provenance verification accepts the publish. - Postgres schema gained the
signaturecolumn that the store queries already referenced. - MinIO service swapped from the now-vanished bitnami tag to the
official
minio/minioimage, with bucket creation viamc mbin a workflow step. - A flaky scheduler test that raced with
t.TempDircleanup now waits for the goroutine to finish on cancel.
0.1.0 - 2026-05-11
Initial release. Covers the full v1 surface described in the project specification, across three binaries (podium, podium-server, podium-mcp) and two SDKs (podium-sdk on PyPI, @lennylabs/podium-sdk on npm).
What's included
- Filesystem mode:
podium syncmaterializes an effective view from a local artifact directory through the configuredHarnessAdapter. Built-in adapters:none,claude-code,claude-desktop,claude-cowork,cursor,codex,gemini,opencode,pi,hermes. - Server mode:
podium serveruns the registry HTTP API. Standalone bootstrap uses embedded SQLite +sqlite-vec; standard deployment wires Postgres +pgvector+ S3-compatible object storage + an OIDC identity provider. LayerComposerwith visibility filtering acrosspublic/organization/ OIDCgroups/ explicitusers.- Domain composition:
DOMAIN.mdparsing, glob resolution, cross-layer merge,extends:resolution, discovery rendering. - Versioning and immutability: semver, content-hash cache keys,
latestresolution withsession_idconsistency, tolerant force-push handling. - Workspace overlay with local BM25 search alongside the registry's hybrid retrieval.
- MCP server:
podium-mcpexposessearch_artifacts,load_artifact,search_domains,load_domainwith materialization through the configured adapter. - Identity: OAuth device-code flow with OS keychain storage; injected-session-token flow for service runtimes.
- SCIM 2.0 + OIDC group claim mapping.
- Audit log with hash-chain integrity, retention policies, and GDPR right-to-be-forgotten.
- Signing: Sigstore keyless by default; pluggable
SignatureProvider. - Dependency graph: cross-type reverse index + impact analysis CLI.
- SDKs:
podium-sdk(Python) and@lennylabs/podium-sdk(TypeScript) as thin HTTP clients. - Plugin surface: every SPI documented in
docs/deployment/extending.md, includingLayerSourceProvider,GitProvider,IdentityProvider,HarnessAdapter,MaterializationHook,SignatureProvider,NotificationProvider, plus search and embedding providers.