@kici-dev/compiler 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/auth/headless-detect.js +1 -1
  2. package/dist/cli-banner.js +1 -1
  3. package/dist/cli.d.ts +18 -0
  4. package/dist/cli.js +71 -10
  5. package/dist/commands/approve.js +2 -2
  6. package/dist/commands/check-mode.js +1 -1
  7. package/dist/commands/compile.js +1 -1
  8. package/dist/commands/diagnostics.js +1 -1
  9. package/dist/commands/docs.js +1 -1
  10. package/dist/commands/drain-worker.js +1 -1
  11. package/dist/commands/endpoints.js +1 -1
  12. package/dist/commands/fixture.js +2 -2
  13. package/dist/commands/held-run-client.d.ts +1 -1
  14. package/dist/commands/held-run-client.js +1 -1
  15. package/dist/commands/hook.js +1 -1
  16. package/dist/commands/index.d.ts +5 -3
  17. package/dist/commands/index.js +5 -4
  18. package/dist/commands/init.js +3 -3
  19. package/dist/commands/login.js +1 -1
  20. package/dist/commands/logout.js +1 -1
  21. package/dist/commands/orchestrators.js +1 -1
  22. package/dist/commands/org.js +1 -1
  23. package/dist/commands/pat.d.ts +27 -0
  24. package/dist/commands/pat.js +76 -0
  25. package/dist/commands/preview.d.ts +88 -0
  26. package/dist/commands/{test.js → preview.js} +16 -15
  27. package/dist/commands/reject.js +2 -2
  28. package/dist/commands/run-hold-watch.d.ts +10 -1
  29. package/dist/commands/run-hold-watch.js +13 -12
  30. package/dist/commands/run.d.ts +20 -1
  31. package/dist/commands/run.js +62 -12
  32. package/dist/commands/runs/cancel.js +1 -1
  33. package/dist/commands/runs/list.js +1 -1
  34. package/dist/commands/runs/logs.js +1 -1
  35. package/dist/commands/runs/rerun.js +1 -1
  36. package/dist/commands/runs/show.js +1 -1
  37. package/dist/commands/secrets-list.js +1 -1
  38. package/dist/commands/types.js +1 -1
  39. package/dist/commands/verify-attestation.d.ts +4 -1
  40. package/dist/commands/verify-attestation.js +34 -12
  41. package/dist/commands/watch.js +1 -1
  42. package/dist/commands/workflows.js +1 -1
  43. package/dist/errors/capability-gap.js +1 -1
  44. package/dist/errors/formatter.js +1 -1
  45. package/dist/errors/index.js +1 -1
  46. package/dist/execution/executor.d.ts +5 -6
  47. package/dist/execution/executor.js +6 -7
  48. package/dist/execution/index.js +1 -1
  49. package/dist/execution/sdk-alias.d.ts +4 -14
  50. package/dist/execution/sdk-alias.js +6 -45
  51. package/dist/execution/ts-loader.js +1 -1
  52. package/dist/fixtures/compiler.js +1 -1
  53. package/dist/fixtures/defaults/index.js +1 -1
  54. package/dist/fixtures/describe-event.d.ts +0 -4
  55. package/dist/fixtures/describe-event.js +35 -5
  56. package/dist/fixtures/picker.js +1 -1
  57. package/dist/format.js +1 -1
  58. package/dist/generators/secrets-dts.js +3 -1
  59. package/dist/hooks/detector.js +1 -1
  60. package/dist/hooks/index.js +1 -1
  61. package/dist/hooks/installer.js +1 -1
  62. package/dist/hooks/templates.js +1 -1
  63. package/dist/index.d.ts +2 -2
  64. package/dist/index.js +3 -3
  65. package/dist/llm-context/llms-architecture.txt +4 -4
  66. package/dist/llm-context/llms-cli.txt +346 -33
  67. package/dist/llm-context/llms-features.txt +128 -21
  68. package/dist/llm-context/llms-full.txt +610 -71
  69. package/dist/llm-context/llms-getting-started.txt +7 -7
  70. package/dist/llm-context/llms-sdk.txt +125 -6
  71. package/dist/llm-context/llms.txt +7 -5
  72. package/dist/local-executor/dag-scheduler.js +1 -1
  73. package/dist/local-executor/index.js +3 -2
  74. package/dist/local-executor/job-runner.js +4 -4
  75. package/dist/local-executor/materializer.js +1 -1
  76. package/dist/local-executor/output-streamer.js +1 -1
  77. package/dist/local-executor/payload-generator.js +1 -1
  78. package/dist/local-executor/picker.js +1 -1
  79. package/dist/local-executor/runs-on-display.js +1 -1
  80. package/dist/local-executor/secret-loader.js +1 -1
  81. package/dist/local-executor/to-event-payload.js +1 -1
  82. package/dist/local-executor/types.js +1 -1
  83. package/dist/local-executor/workflow-lock.js +0 -0
  84. package/dist/lockfile/generator.d.ts +10 -2
  85. package/dist/lockfile/generator.js +107 -53
  86. package/dist/lockfile/hash-files.js +1 -1
  87. package/dist/lockfile/hasher.js +1 -1
  88. package/dist/lockfile/index.js +1 -1
  89. package/dist/lockfile/purity-analyzer.js +3 -3
  90. package/dist/postinstall.js +1 -1
  91. package/dist/provenance-trust-root.js +1 -1
  92. package/dist/remote/config.js +1 -1
  93. package/dist/remote/dashboard-client.js +1 -1
  94. package/dist/remote/encryption.js +1 -1
  95. package/dist/remote/history.d.ts +1 -1
  96. package/dist/remote/history.js +2 -2
  97. package/dist/remote/local-repo-identity.d.ts +32 -0
  98. package/dist/remote/local-repo-identity.js +74 -0
  99. package/dist/remote/oauth.js +1 -1
  100. package/dist/remote/oidc-discovery.js +1 -1
  101. package/dist/remote/output/json.js +1 -1
  102. package/dist/remote/output/junit.js +1 -1
  103. package/dist/remote/output/streaming.js +1 -1
  104. package/dist/remote/output/summary.js +1 -1
  105. package/dist/remote/platform-client.d.ts +2 -0
  106. package/dist/remote/platform-client.js +1 -1
  107. package/dist/remote/prod-defaults.d.ts +9 -1
  108. package/dist/remote/prod-defaults.js +11 -3
  109. package/dist/remote/render.js +1 -1
  110. package/dist/remote/secret-upload.js +1 -1
  111. package/dist/remote/uploader.js +4 -4
  112. package/dist/{chunk-BTugEXQM.js → rolldown-runtime-ClRpJifh.js} +0 -1
  113. package/dist/templates/agents-md.d.ts +1 -1
  114. package/dist/templates/agents-md.js +3 -3
  115. package/dist/templates/index.js +1 -1
  116. package/dist/templates/package-json.js +2 -2
  117. package/dist/templates/tsconfig-json.js +1 -1
  118. package/dist/templates/workflows/hello-world.js +1 -1
  119. package/dist/templates/workflows/pr-checks.js +1 -1
  120. package/dist/test-runner/dry-run.js +1 -1
  121. package/dist/test-runner/event-types.js +1 -1
  122. package/dist/test-runner/git-detector.js +1 -1
  123. package/dist/test-runner/index.js +1 -1
  124. package/dist/test-runner/job-executor.js +1 -1
  125. package/dist/test-runner/output-formatter.js +1 -1
  126. package/dist/test-runner/payload-builder.js +1 -1
  127. package/dist/test-runner/rule-evaluator.js +1 -1
  128. package/dist/test-runner/secrets-file.js +1 -1
  129. package/dist/test-runner/step-context.d.ts +1 -1
  130. package/dist/test-runner/step-context.js +3 -2
  131. package/dist/types.d.ts +33 -6
  132. package/dist/types.js +6 -2
  133. package/dist/validation/index.js +1 -1
  134. package/dist/validation/validator.js +1 -1
  135. package/package.json +4 -7
  136. package/sbom.spdx.json +35 -35
  137. package/dist/commands/held-run-resolve.js +0 -53
@@ -641,7 +641,7 @@ env: async (event) => {
641
641
 
642
642
  Source: https://docs.kici.dev/user/env-vars/
643
643
 
644
- The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) defaults `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to the hosted KiCI Platform, so `kici login` works with no configuration. Set them only to target a self-hosted Platform or a testing environment.
644
+ The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) defaults `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to the hosted KiCI Platform, so `kici login` works with no configuration. Set them only to target another KiCI environment (e.g. a testing instance) or a custom OIDC provider.
645
645
 
646
646
  ## Authentication
647
647
 
@@ -676,9 +676,9 @@ Authenticate with a pre-existing API key (no browser needed):
676
676
  kici login --token <<< "$KICI_API_KEY"
677
677
  ```
678
678
 
679
- ### Self-hosted Platform or custom OIDC provider
679
+ ### Targeting another environment or custom OIDC provider
680
680
 
681
- `kici login` targets the hosted KiCI Platform by default. To point the CLI at a self-hosted Platform or a testing OIDC provider, override the defaults:
681
+ `kici login` targets the hosted KiCI Platform by default. To point the CLI at another KiCI environment (e.g. a testing instance) or a custom OIDC provider, override the defaults:
682
682
 
683
683
  ```bash
684
684
  export KICI_OIDC_ISSUER=https://your-idp.example.com
@@ -802,6 +802,36 @@ job('deploy-review', {
802
802
 
803
803
  A pure function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated inline at dispatch with no init-job overhead. Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
804
804
 
805
+ ### Multiple environments per job
806
+
807
+ A job can bind more than one environment with `environments`, an ordered array. This lets a single job draw secrets and variables from several environments at once — for example a shared `staging` environment plus a `my-testing` environment that carries test-only variables:
808
+
809
+ ```typescript
810
+ job('deploy', {
811
+ runsOn: 'default',
812
+ environments: ['staging', 'my-testing'],
813
+ steps: [
814
+ step('deploy', async (ctx) => {
815
+ // ctx.secrets and ctx.env carry the merged set from both environments
816
+ const dbUrl = await ctx.secrets.get('DB_URL');
817
+ }),
818
+ ],
819
+ });
820
+ ```
821
+
822
+ - `environment` (singular) and `environments` (array) are mutually exclusive — setting both is a compile error. `environment: 'staging'` is exactly equivalent to `environments: ['staging']`.
823
+ - Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic environment.
824
+
825
+ **Merge order — last wins.** All bound environments are resolved on every dispatch (webhook, scheduled, and test runs alike) and merged in array order. When the same secret or variable key is defined in more than one environment, the later entry in the array wins. With `environments: ['staging', 'my-testing']`, a key defined in both resolves to `my-testing`'s value; keys defined in only one are preserved. The longest-scope-path-wins rule still applies _within_ each environment.
826
+
827
+ **Protection rules combine all-must-pass.** A job must satisfy **every** bound environment's gates — adding an environment can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all environments; the minimum trust tier is the most restrictive across them; required reviewers are the union of all environments' reviewers; the wait timer is the longest; and the hold expiry is the shortest. If a run is gated out, the rejection names which environment and which rule rejected it (visible via `kici runs show <run-id>` and the run's rejection reason), so a mutually-exclusive set of rules surfaces as a clear failure rather than a silent perpetual rejection.
828
+
829
+ **Skip-on-test (allow-and-warn).** On a test or local run (`kici run remote`, `kici run local`), a bound environment never rejects the run. Any bound environment that disallows local execution (`allowLocalExecution: false`) — or that is not configured — is **skipped**: its variables and secrets are omitted from the merge and its gates are not evaluated. The run proceeds, and a user-visible warning naming the skipped environment(s) is shown both on the `kici run remote` CLI output and on the dashboard run view. This makes the test-only-variables pattern work: with `environments: ['staging', 'my-testing']` where only `my-testing` allows local execution, a test run resolves just `my-testing`'s variables and warns that `staging` was skipped. If every bound environment is skipped, the job runs with no environment variables. This is intentionally different from a fixture `secrets:` mapping, which is fail-closed — see the [testing guide](https://docs.kici.dev/user/testing-guide/).
830
+
831
+ **Unconfigured environments contribute nothing at dispatch.** At dispatch time a bound environment name with no matching configured environment (and no matching glob environment) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic environment resolving to an as-yet-unconfigured name does today.
832
+
833
+ **Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-environment binding: a bound environment that does not exist, a disabled one, or two environments with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the environments, and the rule — for example `unsatisfiable environment binding: job 'deploy' binds environments [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound environments)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
834
+
805
835
  ### Job-level environment variables
806
836
 
807
837
  The `env` property on a job provides static or dynamic environment variables:
@@ -841,25 +871,30 @@ job('deploy', {
841
871
  });
842
872
  ```
843
873
 
844
- If no `concurrencyGroup` is specified, the environment name is used as the default concurrency group.
874
+ If no `concurrencyGroup` is specified, the environment name is used as the default concurrency group. For a job bound to multiple environments, the default is the **first** bound environment's name.
845
875
 
846
876
  ### Step context
847
877
 
848
878
  Inside a step, the `ctx` object provides:
849
879
 
850
- | Property | Type | Description |
851
- | ----------------- | ------------------------------------- | ---------------------------------------------------------------------------- |
852
- | `ctx.environment` | `string \| undefined` | Resolved environment name (undefined for jobs without environment) |
853
- | `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
854
- | `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta) |
855
-
856
- | Method | Returns | Description |
857
- | ------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
858
- | `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
859
- | `await ctx.secrets.expose(key)` | `void` | Inject a secret into the step's environment variables (`ctx.env`). Throws `SecretNotFoundError` if not found. |
860
- | `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
861
- | `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
862
- | `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
880
+ | Property | Type | Description |
881
+ | ----------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
882
+ | `ctx.environment` | `string \| undefined` | Resolved environment name (undefined for jobs without environment) |
883
+ | `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
884
+ | `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
885
+
886
+ | Method | Returns | Description |
887
+ | -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
888
+ | `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
889
+ | `await ctx.secrets.expose(key)` | `void` | Set the secret as an environment variable for this step visible via `ctx.env` and to child processes (`process.env`). Throws `SecretNotFoundError` if not found. |
890
+ | `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
891
+ | `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
892
+ | `ctx.secrets.list()` | `string[]` | Every secret key available to the step, sorted alphabetically. Synchronous, never throws. |
893
+ | `await ctx.secrets.mountFile(opts)` | `{ path }` | Materialise one or more secrets to a per-step tmpfile (auto-removed at step end). See [Secrets → Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files). |
894
+ | `await ctx.secrets.exposeFile(envVar, opts)` | `{ path }` | `mountFile` plus `process.env[envVar] = path`; the env var is unset at step end. |
895
+ | `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
896
+
897
+ The full secrets API — including `SecretFileOptions`, log masking, and the canonical `sops` example — is documented in [Secrets](https://docs.kici.dev/user/secrets/).
863
898
 
864
899
  ## Environment variable merge precedence
865
900
 
@@ -982,6 +1017,12 @@ Each environment has four tabs:
982
1017
 
983
1018
  4. **History** -- view filtered runs targeting this environment.
984
1019
 
1020
+ ### Bound environments on runs
1021
+
1022
+ A job's bound deployment environments are shown as chips on the run detail page (in the job metadata panel) in the order the job declared them, and the distinct set across a run's jobs appears as compact chips on the run list. For a multi-environment job the chips read left-to-right in merge order — later environments override earlier ones on key collisions. A `(dynamic)` chip marks an environment whose name is computed at runtime; it resolves to the real name once the run starts. A job that binds a single environment shows one chip; a job that binds none shows no chip.
1023
+
1024
+ If a multi-environment binding is gated out, the run's failure banner names which environment and which rule rejected it (the same all-must-pass detail surfaced by `kici runs show <run-id>`).
1025
+
985
1026
  ### Secrets management
986
1027
 
987
1028
  Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to environments via bindings:
@@ -2005,11 +2046,34 @@ statement's build context must match the token's identity claims (a mismatch is
2005
2046
  a hard failure).
2006
2047
 
2007
2048
  ```bash
2008
- kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file>
2049
+ kici verify-attestation [artifact] --bundle <path-or-url> [--trust-root <url-or-file>]
2009
2050
  ```
2010
2051
 
2011
- You supply the trusted issuer out-of-band via `--trust-root` the verifier
2012
- never trusts the issuer named inside the token. There are two forms:
2052
+ ### Which trust root do I use?
2053
+
2054
+ The trust root is the **KiCI platform's provenance issuer** — the same hosted
2055
+ KiCI platform you `kici login` against. KiCI attestations are issued by, and
2056
+ verified against, that one issuer; there are no competing "roots" to choose
2057
+ between. So the answer to "shouldn't I just use KiCI as the trust root?" is yes
2058
+ — and that's the **default**: omit `--trust-root` and the verifier checks the
2059
+ bundle against the hosted KiCI platform automatically. You only pass
2060
+ `--trust-root` to verify against a different environment or, more commonly, an
2061
+ offline `{ issuer, jwks }` file for air-gapped checks.
2062
+
2063
+ ### Why you supply it out-of-band
2064
+
2065
+ Given there's a single issuer, why pass it at all instead of letting the
2066
+ verifier read it from the token? Because the issuer named **inside** a token
2067
+ cannot be trusted: a forged bundle could carry a token that names
2068
+ `iss: https://attacker.example` _and_ bundle a key set that "verifies" it,
2069
+ making the whole signature chain circular and self-attesting. The verifier
2070
+ therefore pins to an issuer you supply out-of-band and checks the token against
2071
+ _that_ — the bundle is verified against a key set you trust, not one it shipped
2072
+ with. Naming the trust root is a security requirement, not a multiple-choice
2073
+ question.
2074
+
2075
+ To override the default, supply the trusted issuer via `--trust-root`, in one of
2076
+ two forms:
2013
2077
 
2014
2078
  - **Online — an HTTPS issuer URL.** The verifier fetches
2015
2079
  `<url>/.well-known/openid-configuration`, reads its `issuer` and `jwks_uri`,
@@ -2037,7 +2101,10 @@ when it does not (or on an error such as a missing flag or unreachable trust
2037
2101
  root).
2038
2102
 
2039
2103
  ```bash
2040
- # Verify a bundle against a deployed issuer, digest-checking the artifact:
2104
+ # Default: verify against the hosted KiCI platform (no --trust-root needed):
2105
+ kici verify-attestation ./dist/app.tgz --bundle ./app.tgz.kici.json
2106
+
2107
+ # Override the trust root to verify against a specific issuer:
2041
2108
  kici verify-attestation ./dist/app.tgz \
2042
2109
  --bundle ./app.tgz.kici.json \
2043
2110
  --trust-root https://platform.example/issuer
@@ -2076,6 +2143,44 @@ attestations shows an empty state.
2076
2143
 
2077
2144
  <!-- /help:run-attestations -->
2078
2145
 
2146
+ ## Browsing attestations across runs
2147
+
2148
+ The **Attestations** page (in the org sidebar) lists every build-provenance
2149
+ attestation your organization has produced — not just one run's. It is the
2150
+ supply-chain audit surface: look up "who built `sha256:…`?" by digest, or browse
2151
+ and filter every attestation across all runs.
2152
+
2153
+ <!-- help:attestations-list#browsing-attestations-across-runs -->
2154
+
2155
+ The **Attestations** page lists every build-provenance attestation your organization has produced.
2156
+
2157
+ - **Search** by artifact digest (exact `sha256:…`) or name to trace a specific artifact.
2158
+ - **Filter** by verification status, repository, workflow, job, or date.
2159
+ - Each row's **status badge** is the verdict KiCI recorded when the attestation was produced (`verified`, `failed`, `unverifiable`, or `pending`).
2160
+
2161
+ Open a row for the parsed provenance statement and a live re-verification.
2162
+
2163
+ <!-- /help:attestations-list -->
2164
+
2165
+ The status badge here is the **server-side verdict**, computed once when the
2166
+ attestation was recorded (verify-at-ingest) — so the list stays fast at any
2167
+ size. `verified` means the signature, build identity, and build context all
2168
+ checked out against the provenance issuer; `failed` means verification ran and
2169
+ the bundle did not pass; `unverifiable` means no verdict could be computed (no
2170
+ provenance issuer configured, or its keys could not be read — not a forgery
2171
+ signal); `pending` means the verdict has not been computed yet.
2172
+
2173
+ Opening a row leads to the **attestation detail page**:
2174
+
2175
+ <!-- help:attestation-detail#browsing-attestations-across-runs -->
2176
+
2177
+ This page shows the parsed provenance for one attestation.
2178
+
2179
+ - **Builder identity, source, and build type** come from the signed SLSA statement.
2180
+ - The **stored badge** is the verdict recorded at build time; **Re-verify** runs the check live in your browser against the current signing keys.
2181
+ - **Download** exports the signed bundle for offline verification with `kici verify-attestation`.
2182
+ <!-- /help:attestation-detail -->
2183
+
2079
2184
  ## See also
2080
2185
 
2081
2186
  - [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.attestProvenance` and
@@ -2097,6 +2202,8 @@ Secrets are managed per-environment in the orchestrator (see [operator docs](htt
2097
2202
 
2098
2203
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
2099
2204
 
2205
+ A job can bind several environments with `environments: ['staging', 'my-testing']`; the secret keys from all bound environments are merged in array order, with a later environment's value winning on a key collision. See [Multiple environments per job](https://docs.kici.dev/user/environments/#multiple-environments-per-job).
2206
+
2100
2207
  ## Where secret values come from
2101
2208
 
2102
2209
  Secret values are written either through the dashboard or through `kici-admin` running against the orchestrator. The orchestrator operator decides — per organization — which surface accepts secret writes. From the workflow author's perspective, the resolution path at run time is identical either way; the difference is where you (or your ops team) **enter** the value.