diff --git a/contract-toolkit/CLAUDE.md b/contract-toolkit/CLAUDE.md index 096fe21610..7e2dd799fc 100644 --- a/contract-toolkit/CLAUDE.md +++ b/contract-toolkit/CLAUDE.md @@ -64,6 +64,8 @@ demonstrates distinct feature surface, verified by `tests/*.pkl`. diverse widgets, UIRules, extraNodes. - `bundle`: multi-entrypoint app (crawler + miner); shared credential configmap; per-entrypoint artifact subfolders. +- `credential-configs`: `additionalCredentialConfigs` — more than one credential + configmap per entrypoint, including a bare (wrapper-less) one; hoisted and deduped. - `deploy`: single-pool migration example — KEDA, resources, env, pool-level `overrides`; shows v0.16.x → v0.17.0 migration path. - `pools`: `pools` map (preferred) — named hot/cold worker pools with per-pool `cooldownPeriod` and resources. - `system-app`: `type = "system"` — no `entrypoints` block or card, no credential config, no `uiConfig`; typed `inputs` render `_input.py` with no manifest, and `contract/generated/input.pkl`, synced into the toolkit as `src/system/.pkl` for callers. diff --git a/contract-toolkit/README.md b/contract-toolkit/README.md index 46bbd8e582..8b369e0ed0 100644 --- a/contract-toolkit/README.md +++ b/contract-toolkit/README.md @@ -103,6 +103,7 @@ The `examples/` directory contains executable contracts that teach stable toolki - [`examples/tree-filters/`](examples/tree-filters/) — extraction app whose include/exclude filters are tree pickers with `treeSelection = true`; `AppInputContract` extends `TreeSelectionExtractionInput`. - [`examples/full/`](examples/full/) — every overridable feature: JDBC URL auth, all pipeline steps, diverse widgets, UIRules, extraNodes. - [`examples/bundle/`](examples/bundle/) — multi-entrypoint app (crawler + miner); shared credential configmap; per-entrypoint artifact subfolders. +- [`examples/credential-configs/`](examples/credential-configs/) — `additionalCredentialConfigs`: an entrypoint that emits more than one credential configmap (primary + wrapped delivery + bare object-store), and one whose only credential is the shared bare config. The bundle hoists and dedupes them. See [CredentialConfig](docs/reference.md#credentialconfig--additional-credential-configmaps). - [`examples/card-split/`](examples/card-split/) — two entrypoints where only one is a marketplace UI card (`packageId` on the card entrypoint; route-only entrypoint has none). - [`examples/behind-the-scenes/`](examples/behind-the-scenes/) — single-entrypoint app with `marketplaceCard = false`; routable but no marketplace card or `package_id` emitted. - [`examples/system-app/`](examples/system-app/) — `type = "system"`: an in-tenant app started by other apps' DAG nodes. No `entrypoints` block, no card, no credential config, no manifest. Typed `inputs` (string, number, enum, bounded list/map, nested object) render `_input.py` without a `uiConfig`. They also render `contract/generated/input.pkl`, the typed DAG node that is synced into the toolkit as `src/system/.pkl` for callers to import. @@ -234,7 +235,7 @@ The single entry point for all new native app contracts. Amend this module and d - App metadata: `name`, `displayName`, `connector`, `icon` - Marketplace metadata: `shortDescription`, `longDescription`, `tags` (emitted top-level into `atlan.yaml`; Build & Publish syncs `tags` to the Global Marketplace App row after each publish, where marketplace search reads them as keywords; `atlan app register` sends all three when it creates the app); `docsUrl` (emitted top-level into `atlan.yaml`) -- Credential config: `credentialCommonFields`, `credentialAuthOptions`, `credentialConnectorType` +- Credential config: `credentialCommonFields`, `credentialAuthOptions`, `credentialConnectorType`; extra or bare (wrapper-less) credential configmaps via `additionalCredentialConfigs` - Workflow config: `uiConfig` (form steps and fields using `Widgets.*` types) - Pipeline: typed `pipeline` block (extract → parseQueries → popularity → lineage → publish) - Deployment: `deploy` (typed `DeployConfig?` — app-level `dapr` / `overrides` plus a named `pools` map; pool keys must match `@task(pool="…")` strings) — see `Deployment.pkl`. `dapr.objectstore` defaults to `true` and must stay on: the SDK requires the deployment object store and fails at startup without it (conformance rule K029 flags `false`) diff --git a/contract-toolkit/docs/reference.md b/contract-toolkit/docs/reference.md index 353eb2731f..3604c8fa0b 100644 --- a/contract-toolkit/docs/reference.md +++ b/contract-toolkit/docs/reference.md @@ -884,6 +884,7 @@ class LineagePublishStep { // wraps LineagePublishNode | `credentialAuthHiddenEnumListForCreating` | Listing? | null | Auth-type enum values hidden during credential creation. | | `credentialNamePlaceholder` | String | `"Host Name"` | Placeholder for hidden credential `name` field. | | `credentialUrlGroup` | AdvancedJDBCUrlGroup? | null | Opt-in JDBC Host↔URL credential form. Requires all `credentialAuthOptions` to be `JDBCUrlAuthOption`. | +| `additionalCredentialConfigs` | Listing | `[]` | Extra credential configmaps beyond `.json`; `wrapper` / `identityProperties` reproduce legacy shapes. See [CredentialConfig](#credentialconfig--additional-credential-configmaps). | The auth-type radio's `ui.hidden` is auto-derived from `credentialAuthOptions.length == 1` — when the connector declares a single auth option, the radio is hidden and the default value is still emitted. No manual flag is required. @@ -1438,6 +1439,7 @@ Developers amend this module. It defines the app's identity, credentials, workfl | `credentialNamePlaceholder` | String | `"Host Name"` | Placeholder for the hidden credential `name` field. | | `credentialConnectorDefault` | String? | null | Optional default for the hidden credential `connector` field. | | `credentialUrlGroup` | AdvancedJDBCUrlGroup? | null | Opt-in JDBC Host↔URL credential form. When set, every `credentialAuthOptions` entry must be a `JDBCUrlAuthOption`. See [AdvancedJDBCUrlGroup](#advancedjdbcurlgroup--hostrlarrowurl-jdbc-credential-form). | +| `additionalCredentialConfigs` | Listing | `[]` | Extra credential configmaps beyond `.json`; `wrapper` / `identityProperties` reproduce legacy shapes. See [CredentialConfig](#credentialconfig--additional-credential-configmaps). | The auth-type radio's `ui.hidden` is auto-derived from `credentialAuthOptions.length == 1` — when the connector declares a single auth option, the radio is hidden and the default value is still emitted. No manual flag is required. @@ -1827,6 +1829,8 @@ open class AuthOption { nestedLabel: String = "" // Custom label for nested section wrapper nestedPlaceholder: String? // Custom placeholder for nested section wrapper nestedHidden: Boolean = true // Hide nested auth pane by default + nestedHelp: String? // `ui.help` on the nested pane; omitted when null + nestedWidth: Int? // `ui.grid` on the nested pane; omitted when null nestedExtraLabel: String = "" // Label on the per-option `extra` nested section nestedExtraHeader: String? = "Advanced" // Header on `extra`; null omits it } @@ -1932,6 +1936,95 @@ credentialAuthOptions { --- +## CredentialConfig — Additional Credential Configmaps + +Each contract emits at most one primary credential configmap, +`.json`, built from the top-level `credential*` properties. +List further configmaps in `additionalCredentialConfigs` when one entrypoint's +form selects more than one credential type (`CredentialInput { credType = "" }`), +or selects one that is not its own connection credential, such as an +object-store delivery target. + +```pkl +class CredentialConfig { + name: String // Configmap name = file stem = credType: (atlan|csa)-connectors- + wrapper: Boolean = true // false: no icon/helpdeskLink/logo/connector/defaultConnectorType + identityProperties: Boolean = true // false: no hidden name/connector/connectorType properties + connector: String? // Envelope `connector`; defaults to the app's. Refused when !wrapper + connectorType: String? // Envelope `defaultConnectorType` (default "rest"). Refused when !wrapper + icon: String?; logo: String?; helpdeskLink: String? // Wrapper keys; default to the app's + authOptions: Mapping // Radio options + nested panes + anyOf rules + commonFields: Listing = [] + sharedExtraFields: Listing = [] + authTitle: String = "Authentication" + authDefault: String? // Defaults to the first authOptions key + authRules: Listing?; authPlaceholder: String?; authWidth: Int? + authHiddenEnumListForCreating: Listing? + namePlaceholder: String? // Hidden `name` placeholder (default "Host Name"). Refused when !identityProperties + connectorDefault: String? // Hidden `connector` default; refused when !identityProperties +} +``` + +Each entry renders through the same renderer as the primary config, so the +auth-type radio, panes and `anyOf` rules have the same shape. The knobs mirror the +top-level properties of the same name. `credentialUrlGroup` (JDBC Host↔URL) applies +only to the primary config. + +**`wrapper` and `identityProperties`.** By default an entry carries what the +primary carries: + +- the envelope: top-level `icon`, `helpdeskLink`, `logo`, `connector`, + `defaultConnectorType`; +- hidden `name` / `connector` / `connectorType` form properties. + +Turn either off only to reproduce a legacy credential type whose form never had +it. Neither is neutral for such a type: + +- the SDK configmap route returns `defaultConnectorType` as + `data.defaultConnectorType`; +- the hidden properties are submitted in every credential body the form creates. + +| Legacy shape | `wrapper` | `identityProperties` | +|---|---|---| +| `@atlan/connectors` `csa-connectors-objectstore` (bare `{"config": ...}`) | `false` | `false` | +| Credential.pkl configs, e.g. generic-miner's object-store form (envelope, no identity properties) | `true` | `false` | + +A field that a flag turns off is refused rather than ignored: `connector` / +`connectorType` / `icon` / `logo` / `helpdeskLink` with `wrapper = false`, and +`connectorDefault` / `namePlaceholder` with `identityProperties = false`. + +**Output.** A single-entrypoint contract writes `app/generated/.json` per +entry. A bundle hoists these files to `app/generated/` and dedupes them by name, +as it does the primary. Two entrypoints may declare the same name only if the +rendered content is identical; otherwise generation fails. + +**Unchanged.** The primary config, `credentialFieldName`, `_input.py` and the e2e +credential codegen (`_e2e_credential.py`) describe only the primary credential. +`hasCredentialConfig = false` suppresses only the primary, so a contract whose only +credentials are additional ones still emits them. + +**Refused at generation:** + +- a multi-entrypoint bundle root, which emits no form or credential of its own: + declare the entry on each entrypoint's `contract`; +- no `uiConfig`, since nothing could select the config; +- a name that is not `atlan-connectors-` / `csa-connectors-` plus lowercase + kebab-case (the SDK treats only those stems as credential templates, so any other + stem can be served as the setup form; `/` or `..` would write outside + `app/generated/`); +- duplicate names, or a name that collides with the primary, workflow config or + manifest; +- an entry with `wrapper = true` and no connector on the entry or the app; +- a field the entry's flags make unused (see above). + +Example: `examples/credential-configs/` is a bundle. Its `export` entrypoint emits +a primary, a wrapped delivery config and a shared bare object-store config +(`wrapper = false`, `identityProperties = false`). Its +`report` entrypoint has no primary, only the shared object-store config, which the +bundle writes once. + +--- + ## AdvancedJDBCUrlGroup — Host↔URL JDBC Credential Form Toolkit primitive for SQL-family connectors whose credential form exposes a @@ -4251,6 +4344,12 @@ deploy = new DeployConfig { For Argo-era apps only. Do not use for new native apps (use `FieldSpec` instead). Still used by CSA connector credential configs. +An App.pkl contract that needs a second credential configmap, the job a +`NativeApp.pkl` `additionalOutputFiles` + Credential.pkl pairing used to do, lists it in +`additionalCredentialConfigs` instead. Set `identityProperties = false` to keep +Credential.pkl's output shape (envelope, no hidden identity properties). See +[CredentialConfig](#credentialconfig--additional-credential-configmaps). + Widget classes come from the modern `Widgets.pkl` module (not the older `Config.pkl`), and Credential.pkl re-exports them as unqualified typealiases — mirroring App.pkl — so a credential contract writes `new TextInput { }`, `new PasswordInput { }`, `new NestedInput { }`, etc. diff --git a/contract-toolkit/examples/credential-configs/app.pkl b/contract-toolkit/examples/credential-configs/app.pkl new file mode 100644 index 0000000000..6acba43e71 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app.pkl @@ -0,0 +1,41 @@ +/// Credential-configs example: entrypoints that emit more than one credential +/// configmap, including a bare (wrapper-less) one, via `additionalCredentialConfigs`. +/// +/// - `export` owns a primary credential (`atlan-connectors-credential-configs`) +/// and also emits two additional ones: the shared bare object-store config and +/// a wrapped delivery config. +/// - `report` has no primary credential (`hasCredentialConfig = false`). Its only +/// credential config is the shared bare object-store one. +/// +/// Credential configmaps are hoisted to `app/generated/` and deduped by name, so +/// the object-store config both entrypoints declare is written once. +/// +/// Generated layout: +/// app/generated/atlan-connectors-credential-configs.json ← export primary +/// app/generated/atlan-connectors-credential-configs-delivery.json ← export, wrapped +/// app/generated/csa-connectors-objectstore.json ← both, bare, deduped +/// app/generated/export/{export.json,manifest.json,_input.py,...} +/// app/generated/report/{report.json,manifest.json,_input.py,...} +amends "../../src/App.pkl" + +import "export.pkl" as Export +import "report.pkl" as Report + +name = "credential-configs" +displayName = "Credential Configs Example" +icon = "https://assets.atlan.com/assets/download.svg" +// The bundle root owns no credential config; its entrypoints do. +hasCredentialConfig = false + +entrypoints { + new Entrypoint { + name = "export" + displayName = "Export" + contract = Export + } + new Entrypoint { + name = "report" + displayName = "Report" + contract = Report + } +} diff --git a/contract-toolkit/examples/credential-configs/app.yaml b/contract-toolkit/examples/credential-configs/app.yaml new file mode 100644 index 0000000000..94657b54e2 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app.yaml @@ -0,0 +1,5 @@ +# AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY. +# To regenerate: pkl eval -m . contract/app.pkl +app_name: credential-configs +app_image: ${APP_IMAGE} +app_port: 8000 diff --git a/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs-delivery.json b/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs-delivery.json new file mode 100644 index 0000000000..72a0d56ec9 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs-delivery.json @@ -0,0 +1,163 @@ +{ + "icon": "https://assets.atlan.com/assets/download.svg", + "helpdeskLink": "", + "logo": "https://assets.atlan.com/assets/download.svg", + "connector": "s3", + "defaultConnectorType": "rest", + "config": { + "properties": { + "name": { + "type": "string", + "required": false, + "ui": { + "label": "Name", + "hidden": true, + "placeholder": "Host Name" + } + }, + "connector": { + "type": "string", + "required": false, + "ui": { + "label": "Connector", + "hidden": true, + "placeholder": "Connector" + } + }, + "connectorType": { + "type": "string", + "required": false, + "ui": { + "key": "_host", + "label": "connectorType", + "placeholder": "connectorType", + "hidden": true + } + }, + "auth-type": { + "type": "string", + "enum": [ + "s3", + "email" + ], + "default": "s3", + "required": true, + "enumNames": [ + "S3", + "Email" + ], + "ui": { + "widget": "radio", + "hidden": false, + "label": "Output destination" + } + }, + "s3": { + "type": "object", + "ui": { + "widget": "nested", + "label": "S3 bucket", + "hidden": false, + "nestedValue": false + }, + "properties": { + "username": { + "type": "string", + "ui": { + "widget": "input", + "label": "AWS access key", + "hidden": false, + "grid": 6 + } + }, + "password": { + "type": "string", + "ui": { + "widget": "password", + "label": "AWS secret key", + "hidden": false, + "grid": 6 + } + }, + "extra": { + "type": "object", + "ui": { + "widget": "nested", + "label": "", + "header": "Advanced", + "hidden": false + }, + "properties": { + "s3-bucket-name": { + "type": "string", + "required": true, + "ui": { + "widget": "input", + "label": "S3 bucket name", + "hidden": false, + "placeholder": "my-bucket", + "grid": 4 + } + } + } + } + } + }, + "email": { + "type": "object", + "ui": { + "widget": "nested", + "label": "Email", + "hidden": false, + "nestedValue": false + }, + "properties": { + "extra": { + "type": "object", + "ui": { + "widget": "nested", + "label": "", + "header": "Advanced", + "hidden": false + }, + "properties": { + "recipients": { + "type": "string", + "required": true, + "ui": { + "widget": "input", + "label": "Recipients", + "hidden": false, + "placeholder": "a@example.com", + "grid": 8 + } + } + } + } + } + } + }, + "anyOf": [ + { + "properties": { + "auth-type": { + "const": "s3" + } + }, + "required": [ + "s3" + ] + }, + { + "properties": { + "auth-type": { + "const": "email" + } + }, + "required": [ + "email" + ] + } + ] + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs.json b/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs.json new file mode 100644 index 0000000000..115346e7f6 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/atlan-connectors-credential-configs.json @@ -0,0 +1,88 @@ +{ + "icon": "https://assets.atlan.com/assets/download.svg", + "helpdeskLink": "", + "logo": "https://assets.atlan.com/assets/download.svg", + "connector": "s3", + "defaultConnectorType": "rest", + "config": { + "properties": { + "name": { + "type": "string", + "required": false, + "ui": { + "label": "Name", + "hidden": true, + "placeholder": "Host Name" + } + }, + "connector": { + "type": "string", + "required": false, + "ui": { + "label": "Connector", + "hidden": true, + "placeholder": "Connector" + } + }, + "connectorType": { + "type": "string", + "required": false, + "ui": { + "key": "_host", + "label": "connectorType", + "placeholder": "connectorType", + "hidden": true + } + }, + "auth-type": { + "type": "string", + "enum": [ + "api" + ], + "default": "api", + "required": true, + "enumNames": [ + "API token" + ], + "ui": { + "widget": "radio", + "hidden": true, + "label": "Authentication" + } + }, + "api": { + "type": "object", + "ui": { + "widget": "nested", + "label": "", + "hidden": true, + "nestedValue": false + }, + "properties": { + "password": { + "type": "string", + "required": true, + "ui": { + "widget": "password", + "label": "Atlan API token", + "hidden": false, + "grid": 6 + } + } + } + } + }, + "anyOf": [ + { + "properties": { + "auth-type": { + "const": "api" + } + }, + "required": [ + "api" + ] + } + ] + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/csa-connectors-objectstore.json b/contract-toolkit/examples/credential-configs/app/generated/csa-connectors-objectstore.json new file mode 100644 index 0000000000..7b99f479da --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/csa-connectors-objectstore.json @@ -0,0 +1,299 @@ +{ + "config": { + "properties": { + "auth-type": { + "type": "string", + "enum": [ + "s3", + "gcs", + "adls" + ], + "default": "s3", + "required": true, + "enumNames": [ + "S3", + "GCS", + "ADLS" + ], + "ui": { + "widget": "radio", + "hidden": false, + "label": "Cloud object store", + "grid": 8 + } + }, + "s3": { + "type": "object", + "ui": { + "widget": "nested", + "label": "S3", + "hidden": false, + "help": "Details for accessing information from Amazon S3.", + "grid": 8, + "nestedValue": false + }, + "properties": { + "username": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "AWS access key", + "hidden": false, + "help": "Enter your AWS access key.", + "placeholder": "", + "grid": 4 + } + }, + "password": { + "type": "string", + "required": false, + "ui": { + "widget": "password", + "label": "AWS secret key", + "hidden": false, + "help": "Enter your AWS secret key.", + "grid": 4 + } + }, + "extra": { + "type": "object", + "required": false, + "ui": { + "widget": "nested", + "label": "S3 details", + "hidden": false, + "help": "", + "grid": 8, + "nestedValue": false + }, + "properties": { + "aws_role_arn": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "AWS Role ARN", + "hidden": false, + "help": "ARN of the AWS role to assume", + "placeholder": "arn:aws:iam::123456789012:role/roleName", + "grid": 8, + "rules": [ + { + "message": "Please enter a valid role ARN", + "pattern": "^arn:aws:iam::\\d{12}:role/.+", + "trigger": "blur" + } + ] + } + }, + "region": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Region", + "hidden": false, + "help": "Enter your AWS region.", + "placeholder": "us-west-1", + "grid": 4 + } + }, + "s3_bucket": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Bucket", + "hidden": false, + "help": "Enter the bucket from which to retrieve the object store object(s).", + "placeholder": "bucket-name", + "grid": 4 + } + } + } + } + } + }, + "gcs": { + "type": "object", + "ui": { + "widget": "nested", + "label": "GCS", + "hidden": false, + "help": "Details for accessing information from Google Cloud Storage (GCS).", + "grid": 8, + "nestedValue": false + }, + "properties": { + "username": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Project ID", + "hidden": false, + "help": "Enter the ID of the GCP project.", + "placeholder": "", + "grid": 4 + } + }, + "password": { + "type": "string", + "required": false, + "ui": { + "widget": "password", + "label": "Service account JSON", + "hidden": false, + "help": "Enter the JSON for your service account credentials.", + "grid": 8 + } + }, + "extra": { + "type": "object", + "required": false, + "ui": { + "widget": "nested", + "label": "GCS details", + "hidden": false, + "help": "", + "grid": 8, + "nestedValue": false + }, + "properties": { + "gcs_bucket": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Bucket", + "hidden": false, + "help": "Enter the bucket from which to retrieve the object store object(s).", + "placeholder": "bucket-name", + "grid": 4 + } + } + } + } + } + }, + "adls": { + "type": "object", + "ui": { + "widget": "nested", + "label": "ADLS", + "hidden": false, + "help": "Details for access information from Azure Data Lake Storage (ADLS).", + "grid": 8, + "nestedValue": false + }, + "properties": { + "username": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Azure client ID", + "hidden": false, + "help": "Enter the unique application (client) ID assigned to your app by Azure AD when the app was registered.", + "placeholder": "", + "grid": 4 + } + }, + "password": { + "type": "string", + "required": false, + "ui": { + "widget": "password", + "label": "Azure client secret", + "hidden": false, + "help": "Enter your client secret.", + "grid": 4 + } + }, + "extra": { + "type": "object", + "required": false, + "ui": { + "widget": "nested", + "label": "ADLS details", + "hidden": false, + "help": "", + "grid": 8, + "nestedValue": false + }, + "properties": { + "azure_tenant_id": { + "type": "string", + "required": false, + "ui": { + "widget": "password", + "label": "Azure tenant ID", + "hidden": false, + "help": "Enter the unique identifier of the Azure Active Directory instance.", + "grid": 4 + } + }, + "storage_account_name": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Storage account name", + "hidden": false, + "help": "Enter the name of your storage account.", + "placeholder": "", + "grid": 4 + } + }, + "adls_container": { + "type": "string", + "required": false, + "ui": { + "widget": "input", + "label": "Container", + "hidden": false, + "help": "Enter the container from which to retrieve the object store object(s).", + "placeholder": "container-name", + "grid": 4 + } + } + } + } + } + } + }, + "anyOf": [ + { + "properties": { + "auth-type": { + "const": "s3" + } + }, + "required": [ + "s3" + ] + }, + { + "properties": { + "auth-type": { + "const": "gcs" + } + }, + "required": [ + "gcs" + ] + }, + { + "properties": { + "auth-type": { + "const": "adls" + } + }, + "required": [ + "adls" + ] + } + ] + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/__init__.py b/contract-toolkit/examples/credential-configs/app/generated/export/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_base.py b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_base.py new file mode 100644 index 0000000000..82d95016f5 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_base.py @@ -0,0 +1,29 @@ +# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT. +# Regenerate with: pkl eval -m . contract/app.pkl +from application_sdk.testing.e2e import BaseE2ETest + + +class ExportGeneratedE2EBase(BaseE2ETest): + connector_short_name = "credential-configs" + argo_package_name = "@atlan/credential-configs" + argo_template_name = "atlan-credential-configs" + app_service_url = ( + "http://credential-configs.credential-configs-app.svc.cluster.local" + ) + connection_type = "s3" + connection_category = "ObjectStore" + connector_config_name = "atlan-connectors-credential-configs" + # This entrypoint's own manifest. Without it the harness reads the + # single-entrypoint default (app/generated/manifest.json), which a bundle + # does not have, and fails with ManifestFileNotFoundError. + manifest_path = "app/generated/export/manifest.json" + # Sent as metadata.entrypoint on the AE submit so Automation Engine + # fetches THIS entrypoint's manifest from the deployed pod. A bare fetch + # 404s "No manifest available" on a multi-entrypoint app. + entrypoint = "export" + # Derived from this entrypoint's pipeline, so the assertions match the + # DAG that was generated rather than the crawler-shaped SDK defaults. + required_dag_nodes = ("extract", "publish") + expect_connection = True + require_nonempty_assets = True + expect_lineage = False diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_credential.py b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_credential.py new file mode 100644 index 0000000000..78b8cecd1b --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_credential.py @@ -0,0 +1,19 @@ +# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT. +# Regenerate with: pkl eval -m . contract/app.pkl +from application_sdk.testing.e2e.credential import CredentialBody +from pydantic import Field + + +class ExportCredentialBody(CredentialBody): + name: str = Field(alias="name") + auth_type: str = Field(default="api", alias="authType") + password: str = Field(default="", alias="password") + + +class ExportAgentCredentialBody(CredentialBody): + name: str = Field(alias="name") + auth_type: str = Field(default="api", alias="authType") + connector_config_name: str = Field( + default="atlan-connectors-credential-configs", alias="connectorConfigName" + ) + extra: dict = Field(default_factory=dict, alias="extra") diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_substitutions.py b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_substitutions.py new file mode 100644 index 0000000000..c968c667ac --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/_e2e_substitutions.py @@ -0,0 +1,17 @@ +# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT. +# Regenerate with: pkl eval -m . contract/app.pkl +from __future__ import annotations + +from application_sdk.testing.e2e.substitutions import MustacheSubstitutions +from pydantic import Field + + +class ExportMustacheSubstitutions(MustacheSubstitutions): + delivery_credential_guid: str = Field( + default="", + alias="{{delivery-credential-guid}}", + ) + objectstore_credential_guid: str = Field( + default="", + alias="{{objectstore-credential-guid}}", + ) diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/_input.py b/contract-toolkit/examples/credential-configs/app/generated/export/_input.py new file mode 100644 index 0000000000..bc7255baa6 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/_input.py @@ -0,0 +1,32 @@ +# AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY. +# To regenerate: pkl eval -m . contract/app.pkl +from __future__ import annotations + +from typing import ClassVar + +from application_sdk.credentials.ref import CredentialRef +from application_sdk.templates.contracts import ExtractionInput + + +class ExportAppInputContract(ExtractionInput): + _config_hash_exclude: ClassVar[set[str]] = { + "output_dir", + "checkpoint_dir", + "load_to_atlan", + "publish_dry_run", + } + + delivery_credential_guid: str = "" + objectstore_credential_guid: str = "" + export_credential: CredentialRef | None = None + output_dir: str = "" + """Directory for output JSONL files.""" + checkpoint_dir: str = "" + """Directory for checkpoint database. If provided, enables incremental extraction.""" + load_to_atlan: bool = True + """If True, load extracted metadata to Atlan via publish-app.""" + publish_dry_run: bool = False + """When True, skip the Atlas publish step (executor_enabled=False).""" + + +AppInputContract = ExportAppInputContract diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/export.json b/contract-toolkit/examples/credential-configs/app/generated/export/export.json new file mode 100644 index 0000000000..0af3ba67ba --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/export.json @@ -0,0 +1,67 @@ +{ + "id": "export", + "name": "Export", + "logo": "https://assets.atlan.com/assets/download.svg", + "config": { + "properties": { + "credential-guid": { + "type": "string", + "required": false, + "ui": { + "widget": "credential", + "label": "", + "hidden": false, + "help": "", + "grid": 8, + "credentialType": "atlan-connectors-credential-configs", + "isTestAuthenticationDisabled": false + } + }, + "delivery-credential-guid": { + "type": "string", + "required": false, + "ui": { + "widget": "credential", + "label": "", + "hidden": false, + "help": "", + "grid": 8, + "credentialType": "atlan-connectors-credential-configs-delivery", + "isTestAuthenticationDisabled": false + } + }, + "objectstore-credential-guid": { + "type": "string", + "required": false, + "ui": { + "widget": "credential", + "label": "", + "hidden": false, + "help": "", + "grid": 8, + "credentialType": "csa-connectors-objectstore", + "isTestAuthenticationDisabled": false + } + } + }, + "steps": [ + { + "title": "Credential", + "description": "", + "id": "credential", + "properties": [ + "credential-guid" + ] + }, + { + "title": "Delivery", + "description": "", + "id": "delivery", + "properties": [ + "delivery-credential-guid", + "objectstore-credential-guid" + ] + } + ] + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/export/manifest.json b/contract-toolkit/examples/credential-configs/app/generated/export/manifest.json new file mode 100644 index 0000000000..a56ccb0b7b --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/export/manifest.json @@ -0,0 +1,60 @@ +{ + "execution_mode": "automation-engine", + "dag": { + "extract": { + "activity_name": "execute_workflow", + "activity_display_name": "Extract Export Metadata", + "app_name": "export", + "inputs": { + "workflow_type": "export", + "app_name": "export", + "app_id": "", + "argo_workflow_slug": "", + "task_queue": "atlan-export-{deployment_name}", + "args": { + "app_name": "export", + "credential": "{{credential}}", + "agent_json": "{{agent-json}}", + "extraction_method": "{{extraction-method}}", + "credential_guid": "{{credential-guid}}", + "delivery_credential_guid": "{{delivery-credential-guid}}", + "objectstore_credential_guid": "{{objectstore-credential-guid}}" + } + }, + "error_handling": { + "start_to_close_timeout_seconds": 86400 + } + }, + "publish": { + "activity_name": "execute_workflow", + "activity_display_name": "Publish to Atlas", + "app_name": "publish", + "inputs": { + "workflow_type": "PublishWorkflow", + "app_name": "publish", + "task_queue": "atlan-publish-{deployment_name}", + "args": { + "connection_qualified_name": "$.extract.outputs.connection_qualified_name", + "transformed_data_prefix": "$.extract.outputs.transformed_data_prefix", + "publish_state_prefix": "$.extract.outputs.publish_state_prefix", + "current_state_prefix": "$.extract.outputs.current_state_prefix", + "connection_creation_enabled": true, + "executor_enabled": true, + "connection_entity": "{{connection}}", + "connection_cache_enabled": true, + "connection_cache_via_app_enabled": true, + "current_state_enabled": true, + "current_state_via_app_enabled": true, + "app_name": "publish" + } + }, + "depends_on": { + "node_id": "extract", + "tag": "success" + }, + "error_handling": { + "start_to_close_timeout_seconds": 259200 + } + } + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/__init__.py b/contract-toolkit/examples/credential-configs/app/generated/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_base.py b/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_base.py new file mode 100644 index 0000000000..a41d908a3b --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_base.py @@ -0,0 +1,27 @@ +# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT. +# Regenerate with: pkl eval -m . contract/app.pkl +from application_sdk.testing.e2e import BaseE2ETest + + +class ReportGeneratedE2EBase(BaseE2ETest): + connector_short_name = "credential-configs" + argo_package_name = "@atlan/credential-configs" + argo_template_name = "atlan-credential-configs" + app_service_url = ( + "http://credential-configs.credential-configs-app.svc.cluster.local" + ) + connector_config_name = "atlan-connectors-credential-configs" + # This entrypoint's own manifest. Without it the harness reads the + # single-entrypoint default (app/generated/manifest.json), which a bundle + # does not have, and fails with ManifestFileNotFoundError. + manifest_path = "app/generated/report/manifest.json" + # Sent as metadata.entrypoint on the AE submit so Automation Engine + # fetches THIS entrypoint's manifest from the deployed pod. A bare fetch + # 404s "No manifest available" on a multi-entrypoint app. + entrypoint = "report" + # Derived from this entrypoint's pipeline, so the assertions match the + # DAG that was generated rather than the crawler-shaped SDK defaults. + required_dag_nodes = ("extract", "publish") + expect_connection = True + require_nonempty_assets = True + expect_lineage = False diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_substitutions.py b/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_substitutions.py new file mode 100644 index 0000000000..f2f1b6e32d --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/report/_e2e_substitutions.py @@ -0,0 +1,13 @@ +# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT. +# Regenerate with: pkl eval -m . contract/app.pkl +from __future__ import annotations + +from application_sdk.testing.e2e.substitutions import MustacheSubstitutions +from pydantic import Field + + +class ReportMustacheSubstitutions(MustacheSubstitutions): + objectstore_credential_guid: str = Field( + default="", + alias="{{objectstore-credential-guid}}", + ) diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/_input.py b/contract-toolkit/examples/credential-configs/app/generated/report/_input.py new file mode 100644 index 0000000000..d116f01eb7 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/report/_input.py @@ -0,0 +1,29 @@ +# AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY. +# To regenerate: pkl eval -m . contract/app.pkl +from __future__ import annotations + +from typing import ClassVar + +from application_sdk.templates.contracts import ExtractionInput + + +class ReportAppInputContract(ExtractionInput): + _config_hash_exclude: ClassVar[set[str]] = { + "output_dir", + "checkpoint_dir", + "load_to_atlan", + "publish_dry_run", + } + + objectstore_credential_guid: str = "" + output_dir: str = "" + """Directory for output JSONL files.""" + checkpoint_dir: str = "" + """Directory for checkpoint database. If provided, enables incremental extraction.""" + load_to_atlan: bool = True + """If True, load extracted metadata to Atlan via publish-app.""" + publish_dry_run: bool = False + """When True, skip the Atlas publish step (executor_enabled=False).""" + + +AppInputContract = ReportAppInputContract diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/manifest.json b/contract-toolkit/examples/credential-configs/app/generated/report/manifest.json new file mode 100644 index 0000000000..ca91f7a61f --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/report/manifest.json @@ -0,0 +1,58 @@ +{ + "execution_mode": "automation-engine", + "dag": { + "extract": { + "activity_name": "execute_workflow", + "activity_display_name": "Extract Report Metadata", + "app_name": "report", + "inputs": { + "workflow_type": "report", + "app_name": "report", + "app_id": "", + "argo_workflow_slug": "", + "task_queue": "atlan-report-{deployment_name}", + "args": { + "app_name": "report", + "credential": "{{credential}}", + "agent_json": "{{agent-json}}", + "extraction_method": "{{extraction-method}}", + "objectstore_credential_guid": "{{objectstore-credential-guid}}" + } + }, + "error_handling": { + "start_to_close_timeout_seconds": 86400 + } + }, + "publish": { + "activity_name": "execute_workflow", + "activity_display_name": "Publish to Atlas", + "app_name": "publish", + "inputs": { + "workflow_type": "PublishWorkflow", + "app_name": "publish", + "task_queue": "atlan-publish-{deployment_name}", + "args": { + "connection_qualified_name": "$.extract.outputs.connection_qualified_name", + "transformed_data_prefix": "$.extract.outputs.transformed_data_prefix", + "publish_state_prefix": "$.extract.outputs.publish_state_prefix", + "current_state_prefix": "$.extract.outputs.current_state_prefix", + "connection_creation_enabled": true, + "executor_enabled": true, + "connection_entity": "{{connection}}", + "connection_cache_enabled": true, + "connection_cache_via_app_enabled": true, + "current_state_enabled": true, + "current_state_via_app_enabled": true, + "app_name": "publish" + } + }, + "depends_on": { + "node_id": "extract", + "tag": "success" + }, + "error_handling": { + "start_to_close_timeout_seconds": 259200 + } + } + } +} diff --git a/contract-toolkit/examples/credential-configs/app/generated/report/report.json b/contract-toolkit/examples/credential-configs/app/generated/report/report.json new file mode 100644 index 0000000000..f1e4a2df6c --- /dev/null +++ b/contract-toolkit/examples/credential-configs/app/generated/report/report.json @@ -0,0 +1,32 @@ +{ + "id": "report", + "name": "Report", + "logo": "https://assets.atlan.com/assets/download.svg", + "config": { + "properties": { + "objectstore-credential-guid": { + "type": "string", + "required": false, + "ui": { + "widget": "credential", + "label": "", + "hidden": false, + "help": "", + "grid": 8, + "credentialType": "csa-connectors-objectstore", + "isTestAuthenticationDisabled": false + } + } + }, + "steps": [ + { + "title": "Delivery", + "description": "", + "id": "delivery", + "properties": [ + "objectstore-credential-guid" + ] + } + ] + } +} diff --git a/contract-toolkit/examples/credential-configs/atlan.yaml b/contract-toolkit/examples/credential-configs/atlan.yaml new file mode 100644 index 0000000000..9f48e20735 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/atlan.yaml @@ -0,0 +1,19 @@ +# AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY. +# To regenerate: pkl eval -m . contract/app.pkl +name: credential-configs +display_name: Credential Configs Example +type: connector +execution_mode: native +icon_url: https://assets.atlan.com/assets/download.svg +visibility: public +build_tag: v1 +self_deployed_runtime: true +entrypoints: +- name: export + display_name: Export + icon_url: https://assets.atlan.com/assets/download.svg + type: connector +- name: report + display_name: Report + icon_url: https://assets.atlan.com/assets/download.svg + type: connector diff --git a/contract-toolkit/examples/credential-configs/export.pkl b/contract-toolkit/examples/credential-configs/export.pkl new file mode 100644 index 0000000000..df2799bab3 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/export.pkl @@ -0,0 +1,79 @@ +/// Export entrypoint for the credential-configs example. +/// +/// Emits three credential configmaps: its primary credential, plus two listed in +/// `additionalCredentialConfigs`, a wrapped delivery config and the shared bare +/// object-store config. +amends "../../src/App.pkl" + +import "../../src/Connectors.pkl" +import "objectstore.pkl" as ObjectStore + +name = "export" +displayName = "Export" +icon = "https://assets.atlan.com/assets/download.svg" +connector = Connectors.S3 +// Named after the bundle so every entrypoint can share the credType. +connectorConfigName = "atlan-connectors-credential-configs" + +credentialAuthOptions { + ["api"] = new AuthOption { + label = "API token" + fields { + new FieldSpec { name = "password"; displayName = "Atlan API token"; sensitive = true; width = 6 } + } + } +} + +additionalCredentialConfigs { + // Wrapped: carries the full credential wrapper and hidden identity properties. + // `connector` falls back to the app's. + new CredentialConfig { + name = "atlan-connectors-credential-configs-delivery" + authTitle = "Output destination" + authOptions { + ["s3"] = new AuthOption { + label = "S3" + nestedLabel = "S3 bucket" + nestedHidden = false + fields { + new FieldSpec { name = "username"; displayName = "AWS access key"; required = false; width = 6 } + new FieldSpec { name = "password"; displayName = "AWS secret key"; sensitive = true; required = false; width = 6 } + } + extraFields { + new FieldSpec { name = "s3-bucket-name"; displayName = "S3 bucket name"; placeholder = "my-bucket"; width = 4 } + } + } + ["email"] = new AuthOption { + label = "Email" + nestedLabel = "Email" + nestedHidden = false + extraFields { + new FieldSpec { name = "recipients"; displayName = "Recipients"; placeholder = "a@example.com"; width = 8 } + } + } + } + } + ObjectStore.config +} + +uiConfig = new UIConfig { + tasks { + ["Credential"] { + inputs { + ["credential-guid"] = new CredentialInput { + credType = "atlan-connectors-credential-configs" + } + } + } + ["Delivery"] { + inputs { + ["delivery-credential-guid"] = new CredentialInput { + credType = "atlan-connectors-credential-configs-delivery" + } + ["objectstore-credential-guid"] = new CredentialInput { + credType = "csa-connectors-objectstore" + } + } + } + } +} diff --git a/contract-toolkit/examples/credential-configs/objectstore.pkl b/contract-toolkit/examples/credential-configs/objectstore.pkl new file mode 100644 index 0000000000..d05773a91c --- /dev/null +++ b/contract-toolkit/examples/credential-configs/objectstore.pkl @@ -0,0 +1,102 @@ +// Shared object-store credential config for the credential-configs example. +// +// A bare config (`wrapper = false`, `identityProperties = false`): only +// `{"config": {properties, anyOf}}`, with no credential envelope and no hidden +// name/connector/connectorType properties. This +// is the shape of a legacy marketplace credential type such as +// `csa-connectors-objectstore`, whose existing credentials were created from a +// form without those properties. +// +// Both entrypoints list it in `additionalCredentialConfigs`. The bundle hoists it +// to `app/generated/csa-connectors-objectstore.json` once, because both copies +// render identically. +import "../../src/App.pkl" +import "../../src/Widgets.pkl" + +local function input(label: String, help: String, ph: String, w: Int): App.TextInput = new { + title = label + helpText = help + placeholderText = ph + width = w +} + +local function secret(label: String, help: String, w: Int): App.PasswordInput = new { + title = label + helpText = help + width = w +} + +local function details(label: String, inputs_: Mapping): App.NamedWidget = new { + name = "extra" + widget = new App.NestedInput { + title = label + inputs = inputs_ + } +} + +local function field(n: String, w: App.UIElement): App.NamedWidget = new { name = n; widget = w } + +config: App.CredentialConfig = new { + name = "csa-connectors-objectstore" + wrapper = false + identityProperties = false + authTitle = "Cloud object store" + authDefault = "s3" + authWidth = 8 + authOptions { + ["s3"] = new App.AuthOption { + label = "S3" + nestedLabel = "S3" + nestedHidden = false + nestedHelp = "Details for accessing information from Amazon S3." + nestedWidth = 8 + fields { + field("username", input("AWS access key", "Enter your AWS access key.", "", 4)) + field("password", secret("AWS secret key", "Enter your AWS secret key.", 4)) + details("S3 details", new Mapping { + ["aws_role_arn"] = (input("AWS Role ARN", "ARN of the AWS role to assume", "arn:aws:iam::123456789012:role/roleName", 8)) { + validationRules { + new Widgets.ValidationRule { + pattern = #"^arn:aws:iam::\d{12}:role/.+"# + message = "Please enter a valid role ARN" + trigger = "blur" + } + } + } + ["region"] = input("Region", "Enter your AWS region.", "us-west-1", 4) + ["s3_bucket"] = input("Bucket", "Enter the bucket from which to retrieve the object store object(s).", "bucket-name", 4) + }) + } + } + ["gcs"] = new App.AuthOption { + label = "GCS" + nestedLabel = "GCS" + nestedHidden = false + nestedHelp = "Details for accessing information from Google Cloud Storage (GCS)." + nestedWidth = 8 + fields { + field("username", input("Project ID", "Enter the ID of the GCP project.", "", 4)) + field("password", secret("Service account JSON", "Enter the JSON for your service account credentials.", 8)) + details("GCS details", new Mapping { + ["gcs_bucket"] = input("Bucket", "Enter the bucket from which to retrieve the object store object(s).", "bucket-name", 4) + }) + } + } + ["adls"] = new App.AuthOption { + label = "ADLS" + nestedLabel = "ADLS" + nestedHidden = false + nestedHelp = "Details for access information from Azure Data Lake Storage (ADLS)." + nestedWidth = 8 + fields { + field("username", input("Azure client ID", "Enter the unique application (client) ID assigned to your app by Azure AD when the app was registered.", "", 4)) + field("password", secret("Azure client secret", "Enter your client secret.", 4)) + details("ADLS details", new Mapping { + ["azure_tenant_id"] = secret("Azure tenant ID", "Enter the unique identifier of the Azure Active Directory instance.", 4) + ["storage_account_name"] = input("Storage account name", "Enter the name of your storage account.", "", 4) + ["adls_container"] = input("Container", "Enter the container from which to retrieve the object store object(s).", "container-name", 4) + }) + } + } + } +} diff --git a/contract-toolkit/examples/credential-configs/report.pkl b/contract-toolkit/examples/credential-configs/report.pkl new file mode 100644 index 0000000000..20aeb020e8 --- /dev/null +++ b/contract-toolkit/examples/credential-configs/report.pkl @@ -0,0 +1,29 @@ +/// Report entrypoint for the credential-configs example. +/// +/// Has no primary credential (`hasCredentialConfig = false`). Its only +/// credential configmap is the shared bare object-store config, which the bundle +/// dedupes with the copy `export` declares. +amends "../../src/App.pkl" + +import "objectstore.pkl" as ObjectStore + +name = "report" +displayName = "Report" +icon = "https://assets.atlan.com/assets/download.svg" +hasCredentialConfig = false + +additionalCredentialConfigs { + ObjectStore.config +} + +uiConfig = new UIConfig { + tasks { + ["Delivery"] { + inputs { + ["objectstore-credential-guid"] = new CredentialInput { + credType = "csa-connectors-objectstore" + } + } + } + } +} diff --git a/contract-toolkit/scripts/check-invariants.sh b/contract-toolkit/scripts/check-invariants.sh index 045557ae02..7ccdf76410 100755 --- a/contract-toolkit/scripts/check-invariants.sh +++ b/contract-toolkit/scripts/check-invariants.sh @@ -277,7 +277,7 @@ check_eval_fails() { rm -f "$contract" rm -rf "$out_dir" if ! echo "$err" | grep -q "$expect"; then - echo "FAIL: streaming refusal did not fire for $label (expected: $expect)" + echo "FAIL: eval-time refusal did not fire for $label (expected: $expect)" echo " Got: $err" fail=1 fi @@ -1015,6 +1015,60 @@ fi rm -f "$DD_CTRL_CONTRACT" rm -rf "$DD_CTRL_OUT" +# -------------------------------------------------------------------------- +# 12. FND-4755: additionalCredentialConfigs must be refused, not emitted or +# dropped, when the name would escape app/generated/ (or be served as a +# setup form), or when an entry sets a field its own flags turn off. +# tests/additional_credential_configs_test.pkl covers the rest of the guard. +# -------------------------------------------------------------------------- +# $1 = CredentialConfig body +extra_credential_body() { + cat <