You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sub-issue of #3171 (step 5 of its Suggested Work Breakdown). Follows #3299
(step 3, merged 2026-09-17 via PR #3383). Coordinated pre-0.1.0 breaking
change under #2565.
User Story
As an OpenShell platform operator, I want the behavior that survives a provider profile import to be either fully described by the profile I imported or named as an explicit platform adapter, so that I can tell what a profile actually does without reading the release binary's source.
As a maintainer, I want every compiled-in provider and middleware behavior classified as a platform primitive or example/extension material, so that the import-only boundary established in #3299 is a real contract rather than a boundary around profile YAML alone.
Problem Statement
#3299 is merged (PR #3383, 04146692d on main). It removed the compiled provider profile catalog: providers/*.yaml is no longer include_str!d, the default provider_profile_sources is [{ type = "user" }], and a profile's definition comes only from what an operator imported. That change covered profile data. It did not cover the compiled behavior that a resolved profile ID still activates.
Two type-specific adapters remain in openshell-providers:
crates/openshell-providers/src/providers/google_cloud.rs — projects project_id, region and service_account_email from provider config into GCP SDK environment variables, and unconditionally sets GCE_METADATA_HOST.
crates/openshell-providers/src/providers/vertex.rs — projects VERTEX_AI_PROJECT_ID and VERTEX_AI_REGION into the same GCP variables plus ANTHROPIC_VERTEX_PROJECT_ID, VERTEX_LOCATION, and a GOOSE_PROVIDER default.
ProviderRegistry::new() (crates/openshell-providers/src/lib.rs:89) registers both, and inject_env_for_profile_id fires whichever plugin matches the resolved profile ID exactly. An operator who imports a profile at ID google-cloud therefore gets environment projection that no part of the imported YAML declares, and an operator who imports the same content at ID gcp gets none of it. The profile is no longer the complete definition of the provider, but nothing in the profile or the CLI says so.
The profile contract cannot currently express this. ProviderProfile (proto/openshell.proto:2392) carries credentials, endpoints, binaries, discovery and annotations. The only environment projection it has is ProviderProfileCredential.env_vars, which names the variables that receive a secret value. There is no field for projecting non-secret provider config — a project ID, a region, an SDK selector — into the sandbox environment. That is the schema gap #3171 names: "Type-specific behavior that YAML cannot currently describe — such as Google Cloud metadata and project/region environment projection — must either become expressible in the profile contract or remain a deliberately documented adapter. It must not masquerade as generic profile data."
The same classification is unresolved for middleware. openshell-supervisor-middleware-builtins::services() (crates/openshell-supervisor-middleware-builtins/src/lib.rs:23) installs openshell/regex into every supervisor. It is inert until a policy selects it by name, so it activates nothing by default, but it has never been explicitly classified as a stable platform primitive versus example/extension material.
Impact / Why This Matters
Today an operator reading an imported profile cannot predict the sandbox environment it produces. For google-cloud and google-vertex-ai the imported YAML is an incomplete description, and the missing part is only discoverable by reading the OpenShell source for the exact release in use. That is the failure mode #3299 set out to remove — a shipped definition resident behind an imported one — reintroduced one layer down.
The coupling is also silently ID-keyed. Because activation is exact-match on profile ID with no alias table, renaming a profile on import, or importing an adapted GCP profile under an organization-specific ID, silently drops the environment projection. The provider still creates, still attaches, and still authenticates; the workload fails later when an SDK cannot find its project. Nothing warns, because nothing in the system knows the projection was expected.
The current workaround is to keep using the canonical IDs and to know from experience which ones carry compiled behavior. That is insufficient: it is an unwritten compatibility promise, it does not survive an operator forking a profile, and it leaves the 0.1.0 contract in #2565 claiming an import-only catalog that is import-only only for data.
There is also accumulated residue that makes the boundary harder to read than it is:
ProviderDiscoverySpec and discover_with_spec (crates/openshell-providers/src/lib.rs:65, re-exported at :20) are public API shaped around &'static [&'static str] credential env var tables. After refactor(providers)!: make provider profiles import-only #3299 no in-tree caller constructs a ProviderDiscoverySpec; the live path is discover_from_profile. A compiled-table discovery entrypoint with no caller is exactly the "hidden local copy of the removed catalog" shape the refactor(platform)!: make sandbox environments explicitly composed #3171 acceptance criteria warn about, even though it is currently empty.
proto/openshell.proto:2402 and :2408 still document resource_version and source in terms of built-in profiles and a "builtin" provenance value that the gateway no longer produces and now rejects at config parse time.
Proposed Design
Produce an inventory of every compiled behavior that a resolved provider profile or a policy-selected middleware name can activate, and give each entry one of three dispositions. The inventory is the deliverable; the code changes follow from it.
1. Expressible in the profile contract. The behavior is generic config-to-environment projection that any operator-authored profile should be able to declare. Extend the profile schema so the imported YAML declares it, and delete the compiled special case. For the GCP adapters this means a non-secret environment projection in ProviderProfile — config keys the profile declares, and the environment variables each key populates — so that google-cloud.yaml and google-vertex-ai.yaml state their own projection and an operator can fork them under any ID without losing behavior. Projection must not overwrite an environment variable the caller already set, matching current behavior.
2. Deliberately documented adapter. The behavior is genuinely type-specific and cannot be expressed generically without becoming a scripting surface. It stays compiled, but it becomes visible: the profile it binds to declares that it requires a named platform adapter, the CLI and gateway surface which adapter a profile activates, and importing a profile whose declared adapter is absent fails with a bounded diagnostic rather than silently producing a partially configured provider. GCE_METADATA_HOST and any metadata-emulator coupling are candidates here if the projection approach does not cover them.
3. Remove. The behavior has no remaining caller or no remaining justification. ProviderDiscoverySpec and discover_with_spec are the current candidates.
For middleware, state the classification explicitly in the architecture docs and act on it. Either openshell/regex is a stable platform primitive — in which case say so, and document its config contract as part of the supported policy surface — or it is example material, in which case it moves to the examples/extension layer with its policy-selection path unchanged for operators who configure it. Either way the observable default is unchanged: no middleware activates unless a policy selects it.
The user-facing workflow after this work:
An operator inspects an example profile and sees every environment variable it will set, whether from a credential or from non-secret config.
An operator forks google-cloud.yaml, imports it as acme-gcp, and gets identical behavior to the canonical ID.
An operator importing a profile that requires a platform adapter the gateway does not have gets a bounded error naming the adapter, at import time.
openshell provider profile lint reports a profile that declares an unknown adapter or an environment projection that collides with a credential's env_vars.
Scope note: this issue covers classification, schema expression, and adapter visibility. It does not change credential refresh strategies, which are already declared in the profile (ProviderCredentialRefresh.strategy) and are therefore already expressed rather than implied.
Acceptance Criteria
Every compiled provider behavior reachable from a resolved profile ID is inventoried with a recorded disposition of expressed-in-schema, documented-adapter, or removed.
No provider behavior activates on a resolved profile ID without the imported profile declaring it, either directly or by naming a required platform adapter.
A profile forked from an example and imported under a different ID produces the same sandbox environment as the canonical ID.
Non-secret provider config projection is either declarable in ProviderProfile or documented as an explicit adapter with a named boundary; it is not implied by the profile ID alone.
Environment projection never overwrites a variable already present in the sandbox environment.
Importing or attaching a profile that requires an unavailable platform adapter fails with a bounded diagnostic naming the adapter, rather than creating a partially configured provider.
openshell provider profile lint rejects a profile declaring an unknown adapter or a projection that collides with a credential's env_vars.
ProviderDiscoverySpec and discover_with_spec are removed or have a documented caller.
openshell/regex is explicitly classified in the architecture docs as a platform primitive or as examples/extension material, and the classification is reflected in where it lives.
No middleware or interceptor configuration activates without explicit policy selection; the default posture is unchanged by this work.
providers/google-cloud.yaml and providers/google-vertex-ai.yaml document their full environment effect, matching the smoke-test requirement already applied to the other examples.
Proto documentation for ProviderProfile.source and ProviderProfile.resource_version no longer describes built-in profiles or a "builtin" provenance value.
The 0.1.0 migration notes cover any profile schema addition and any adapter that operators must now declare.
Tests prove a forked-ID profile behaves identically to the canonical ID, and that an absent declared adapter fails closed.
Alternatives Considered
Delete both GCP adapters outright. Cleanest boundary, and it makes the profile the whole definition with no new schema. It also breaks every existing google-cloud and google-vertex-ai provider record at upgrade: the SDKs in those sandboxes lose their project and region and fail at call time. #3171 explicitly preserves "genuine platform capabilities" and asks for classification rather than removal, so unconditional deletion answers the wrong question.
Leave the adapters and document them in the README only. Cheapest option, and it is honest about what the code does. It does not fix the ID coupling — a forked profile still silently loses behavior — and it leaves the release binary as the authoritative source for part of a provider's definition, which is the coupling #3299 removed for data.
Make the profile schema generic enough to express anything. A full templating or scripting surface in profile YAML would cover the GCP cases and every future one. It turns reviewable declarative example data into a program an operator must audit, which defeats the reviewability that motivated import-only profiles. A bounded config-key-to-environment-variable projection covers the observed cases without becoming a scripting surface.
Keep alias fallback so forked IDs still activate adapters. Restoring any form of ID aliasing reintroduces exactly what #3299 removed, and it cannot work for an unbounded operator catalog. An explicit adapter declaration in the profile achieves the same outcome without guessing.
Defer until #3145 lands.#3145 has now landed (PR #3259, 1d010f418 on main), so there is nothing left to defer to. The pre-activation composition gate it added is a natural place to validate a composed adapter requirement, and this issue should declare its requirements in a form that gate can validate.
Agent Investigation
Verified against main at 04146692d (#3299, merged 2026-09-17 via PR #3383):
Activation is exact-match on the gateway-resolved profile ID (inject_env_for_profile_id, crates/openshell-providers/src/lib.rs:116), with no alias table. A profile imported under a non-canonical ID activates no plugin.
ProviderProfile has no non-secret environment projection field. ProviderProfileCredential.env_vars is the only env-var surface and it carries secret values, so the GCP projection cannot currently be moved into YAML without a schema addition.
ProviderDiscoverySpec and discover_with_spec have no in-tree constructor or caller outside their own definition and re-export; discover_from_profile is the live discovery path.
openshell-supervisor-middleware-builtins::services() returns BuiltinMiddlewareService unconditionally, and validate_config/evaluate_http_request dispatch on the middleware name, rejecting anything that is not openshell/regex. Policy selects it by name (crates/openshell-policy/src/lib.rs), and the MXC driver references the same name in its policy mapper, so relocation would need those call sites considered.
proto/openshell.proto:2402 and :2408 retain stale built-in profile documentation. No runtime path produces source == "builtin" after refactor(providers)!: make provider profiles import-only #3383; {"type":"builtin"} is rejected at config parse time (crates/openshell-core/src/config.rs:1161, crates/openshell-server/src/config_file.rs:1110).
Checklist
I've reviewed existing issues and the architecture docs
This is a design proposal, not a "please build this" request
Sub-issue of #3171 (step 5 of its Suggested Work Breakdown). Follows #3299
(step 3, merged 2026-09-17 via PR #3383). Coordinated pre-
0.1.0breakingchange under #2565.
User Story
As an OpenShell platform operator, I want the behavior that survives a provider profile import to be either fully described by the profile I imported or named as an explicit platform adapter, so that I can tell what a profile actually does without reading the release binary's source.
As a maintainer, I want every compiled-in provider and middleware behavior classified as a platform primitive or example/extension material, so that the import-only boundary established in #3299 is a real contract rather than a boundary around profile YAML alone.
Problem Statement
#3299 is merged (PR #3383,
04146692donmain). It removed the compiled provider profile catalog:providers/*.yamlis no longerinclude_str!d, the defaultprovider_profile_sourcesis[{ type = "user" }], and a profile's definition comes only from what an operator imported. That change covered profile data. It did not cover the compiled behavior that a resolved profile ID still activates.Two type-specific adapters remain in
openshell-providers:crates/openshell-providers/src/providers/google_cloud.rs— projectsproject_id,regionandservice_account_emailfrom provider config into GCP SDK environment variables, and unconditionally setsGCE_METADATA_HOST.crates/openshell-providers/src/providers/vertex.rs— projectsVERTEX_AI_PROJECT_IDandVERTEX_AI_REGIONinto the same GCP variables plusANTHROPIC_VERTEX_PROJECT_ID,VERTEX_LOCATION, and aGOOSE_PROVIDERdefault.ProviderRegistry::new()(crates/openshell-providers/src/lib.rs:89) registers both, andinject_env_for_profile_idfires whichever plugin matches the resolved profile ID exactly. An operator who imports a profile at IDgoogle-cloudtherefore gets environment projection that no part of the imported YAML declares, and an operator who imports the same content at IDgcpgets none of it. The profile is no longer the complete definition of the provider, but nothing in the profile or the CLI says so.The profile contract cannot currently express this.
ProviderProfile(proto/openshell.proto:2392) carriescredentials,endpoints,binaries,discoveryandannotations. The only environment projection it has isProviderProfileCredential.env_vars, which names the variables that receive a secret value. There is no field for projecting non-secret provider config — a project ID, a region, an SDK selector — into the sandbox environment. That is the schema gap #3171 names: "Type-specific behavior that YAML cannot currently describe — such as Google Cloud metadata and project/region environment projection — must either become expressible in the profile contract or remain a deliberately documented adapter. It must not masquerade as generic profile data."The same classification is unresolved for middleware.
openshell-supervisor-middleware-builtins::services()(crates/openshell-supervisor-middleware-builtins/src/lib.rs:23) installsopenshell/regexinto every supervisor. It is inert until a policy selects it by name, so it activates nothing by default, but it has never been explicitly classified as a stable platform primitive versus example/extension material.Impact / Why This Matters
Today an operator reading an imported profile cannot predict the sandbox environment it produces. For
google-cloudandgoogle-vertex-aithe imported YAML is an incomplete description, and the missing part is only discoverable by reading the OpenShell source for the exact release in use. That is the failure mode #3299 set out to remove — a shipped definition resident behind an imported one — reintroduced one layer down.The coupling is also silently ID-keyed. Because activation is exact-match on profile ID with no alias table, renaming a profile on import, or importing an adapted GCP profile under an organization-specific ID, silently drops the environment projection. The provider still creates, still attaches, and still authenticates; the workload fails later when an SDK cannot find its project. Nothing warns, because nothing in the system knows the projection was expected.
The current workaround is to keep using the canonical IDs and to know from experience which ones carry compiled behavior. That is insufficient: it is an unwritten compatibility promise, it does not survive an operator forking a profile, and it leaves the
0.1.0contract in #2565 claiming an import-only catalog that is import-only only for data.There is also accumulated residue that makes the boundary harder to read than it is:
ProviderDiscoverySpecanddiscover_with_spec(crates/openshell-providers/src/lib.rs:65, re-exported at:20) are public API shaped around&'static [&'static str]credential env var tables. After refactor(providers)!: make provider profiles import-only #3299 no in-tree caller constructs aProviderDiscoverySpec; the live path isdiscover_from_profile. A compiled-table discovery entrypoint with no caller is exactly the "hidden local copy of the removed catalog" shape the refactor(platform)!: make sandbox environments explicitly composed #3171 acceptance criteria warn about, even though it is currently empty.proto/openshell.proto:2402and:2408still documentresource_versionandsourcein terms of built-in profiles and a"builtin"provenance value that the gateway no longer produces and now rejects at config parse time.Proposed Design
Produce an inventory of every compiled behavior that a resolved provider profile or a policy-selected middleware name can activate, and give each entry one of three dispositions. The inventory is the deliverable; the code changes follow from it.
1. Expressible in the profile contract. The behavior is generic config-to-environment projection that any operator-authored profile should be able to declare. Extend the profile schema so the imported YAML declares it, and delete the compiled special case. For the GCP adapters this means a non-secret environment projection in
ProviderProfile— config keys the profile declares, and the environment variables each key populates — so thatgoogle-cloud.yamlandgoogle-vertex-ai.yamlstate their own projection and an operator can fork them under any ID without losing behavior. Projection must not overwrite an environment variable the caller already set, matching current behavior.2. Deliberately documented adapter. The behavior is genuinely type-specific and cannot be expressed generically without becoming a scripting surface. It stays compiled, but it becomes visible: the profile it binds to declares that it requires a named platform adapter, the CLI and gateway surface which adapter a profile activates, and importing a profile whose declared adapter is absent fails with a bounded diagnostic rather than silently producing a partially configured provider.
GCE_METADATA_HOSTand any metadata-emulator coupling are candidates here if the projection approach does not cover them.3. Remove. The behavior has no remaining caller or no remaining justification.
ProviderDiscoverySpecanddiscover_with_specare the current candidates.For middleware, state the classification explicitly in the architecture docs and act on it. Either
openshell/regexis a stable platform primitive — in which case say so, and document its config contract as part of the supported policy surface — or it is example material, in which case it moves to the examples/extension layer with its policy-selection path unchanged for operators who configure it. Either way the observable default is unchanged: no middleware activates unless a policy selects it.The user-facing workflow after this work:
google-cloud.yaml, imports it asacme-gcp, and gets identical behavior to the canonical ID.openshell provider profile lintreports a profile that declares an unknown adapter or an environment projection that collides with a credential'senv_vars.Scope note: this issue covers classification, schema expression, and adapter visibility. It does not change credential refresh strategies, which are already declared in the profile (
ProviderCredentialRefresh.strategy) and are therefore already expressed rather than implied.Acceptance Criteria
ProviderProfileor documented as an explicit adapter with a named boundary; it is not implied by the profile ID alone.openshell provider profile lintrejects a profile declaring an unknown adapter or a projection that collides with a credential'senv_vars.ProviderDiscoverySpecanddiscover_with_specare removed or have a documented caller.openshell/regexis explicitly classified in the architecture docs as a platform primitive or as examples/extension material, and the classification is reflected in where it lives.providers/google-cloud.yamlandproviders/google-vertex-ai.yamldocument their full environment effect, matching the smoke-test requirement already applied to the other examples.ProviderProfile.sourceandProviderProfile.resource_versionno longer describes built-in profiles or a"builtin"provenance value.0.1.0migration notes cover any profile schema addition and any adapter that operators must now declare.Alternatives Considered
Delete both GCP adapters outright. Cleanest boundary, and it makes the profile the whole definition with no new schema. It also breaks every existing
google-cloudandgoogle-vertex-aiprovider record at upgrade: the SDKs in those sandboxes lose their project and region and fail at call time. #3171 explicitly preserves "genuine platform capabilities" and asks for classification rather than removal, so unconditional deletion answers the wrong question.Leave the adapters and document them in the README only. Cheapest option, and it is honest about what the code does. It does not fix the ID coupling — a forked profile still silently loses behavior — and it leaves the release binary as the authoritative source for part of a provider's definition, which is the coupling #3299 removed for data.
Make the profile schema generic enough to express anything. A full templating or scripting surface in profile YAML would cover the GCP cases and every future one. It turns reviewable declarative example data into a program an operator must audit, which defeats the reviewability that motivated import-only profiles. A bounded config-key-to-environment-variable projection covers the observed cases without becoming a scripting surface.
Keep alias fallback so forked IDs still activate adapters. Restoring any form of ID aliasing reintroduces exactly what #3299 removed, and it cannot work for an unbounded operator catalog. An explicit adapter declaration in the profile achieves the same outcome without guessing.
Defer until #3145 lands. #3145 has now landed (PR #3259,
1d010f418onmain), so there is nothing left to defer to. The pre-activation composition gate it added is a natural place to validate a composed adapter requirement, and this issue should declare its requirements in a form that gate can validate.Agent Investigation
Verified against
mainat04146692d(#3299, merged 2026-09-17 via PR #3383):crates/openshell-providers/src/providers/contains exactly two adapters after the refactor(providers)!: make provider profiles import-only #3299 cleanup —google_cloud.rs(161 lines) andvertex.rs(149 lines). Twelve dead plugin modules were removed in refactor(providers)!: make provider profiles import-only #3383; these two were kept deliberately, with an in-code comment describing them as "legacy config projectors required to run existing Google Cloud and Vertex records."inject_env_for_profile_id,crates/openshell-providers/src/lib.rs:116), with no alias table. A profile imported under a non-canonical ID activates no plugin.ProviderProfilehas no non-secret environment projection field.ProviderProfileCredential.env_varsis the only env-var surface and it carries secret values, so the GCP projection cannot currently be moved into YAML without a schema addition.ProviderDiscoverySpecanddiscover_with_spechave no in-tree constructor or caller outside their own definition and re-export;discover_from_profileis the live discovery path.openshell-supervisor-middleware-builtins::services()returnsBuiltinMiddlewareServiceunconditionally, andvalidate_config/evaluate_http_requestdispatch on the middleware name, rejecting anything that is notopenshell/regex. Policy selects it by name (crates/openshell-policy/src/lib.rs), and the MXC driver references the same name in its policy mapper, so relocation would need those call sites considered.proto/openshell.proto:2402and:2408retain stale built-in profile documentation. No runtime path producessource == "builtin"after refactor(providers)!: make provider profiles import-only #3383;{"type":"builtin"}is rejected at config parse time (crates/openshell-core/src/config.rs:1161,crates/openshell-server/src/config_file.rs:1110).Checklist