@kici-dev/compiler 0.7.0 → 0.9.0
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.
- package/dist/cli.js +5 -4
- package/dist/commands/docs.js +2 -2
- package/dist/commands/feedback.d.ts +23 -1
- package/dist/commands/feedback.js +86 -9
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/local.d.ts +13 -5
- package/dist/commands/local.js +19 -8
- package/dist/commands/report/identity.js +1 -1
- package/dist/commands/run-banner.d.ts +1 -1
- package/dist/commands/run-banner.js +1 -1
- package/dist/commands/run-routed.js +3 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/runs/logs.js +3 -2
- package/dist/commands/verify-attestation.js +1 -1
- package/dist/llm-context/llms-architecture.txt +9 -7
- package/dist/llm-context/llms-cli-remote.txt +27 -12
- package/dist/llm-context/llms-cli.txt +6 -4
- package/dist/llm-context/llms-features-execution.txt +9 -9
- package/dist/llm-context/llms-features.txt +452 -133
- package/dist/llm-context/llms-full.txt +561 -197
- package/dist/llm-context/llms-getting-started.txt +34 -17
- package/dist/llm-context/llms-providers.txt +2 -2
- package/dist/llm-context/llms-sdk-runtime.txt +16 -2
- package/dist/llm-context/llms-sdk.txt +7 -12
- package/dist/llm-context/llms.txt +7 -6
- package/dist/local-plane/orchestrator-process.d.ts +4 -5
- package/dist/local-plane/orchestrator-process.js +5 -1
- package/dist/local-plane/paths.d.ts +1 -0
- package/dist/local-plane/paths.js +1 -0
- package/dist/local-plane/plane-log.d.ts +27 -0
- package/dist/local-plane/plane-log.js +39 -0
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/local-plane/plane-trigger.d.ts +28 -0
- package/dist/local-plane/plane-trigger.js +57 -2
- package/dist/local-plane/postgres.js +9 -6
- package/dist/local-plane/run-follow.js +2 -1
- package/dist/remote/output/streaming.d.ts +12 -0
- package/dist/remote/output/streaming.js +20 -1
- package/dist/remote/platform-client.d.ts +2 -0
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +9 -7
- package/dist/templates/package-json.d.ts +9 -7
- package/dist/templates/package-json.js +11 -9
- package/dist/templates/workflows/hello-world.ts +1 -1
- package/dist/templates/workflows/pr-checks.ts +2 -2
- package/dist/test-runner/job-executor.js +3 -2
- package/dist/types.d.ts +12 -35
- package/dist/types.js +2 -12
- package/dist/types.test-d.d.ts +2 -0
- package/dist/types.test-d.js +63 -0
- package/dist/workflows/hello-world.ts +1 -1
- package/dist/workflows/pr-checks.ts +2 -2
- package/package.json +15 -15
- package/sbom.spdx.json +1303 -1483
|
@@ -590,7 +590,7 @@ Point to a local SDK checkout for IDE autocompletion:
|
|
|
590
590
|
{
|
|
591
591
|
"name": "my-project-kici",
|
|
592
592
|
"devDependencies": {
|
|
593
|
-
"@kici-dev/sdk": "
|
|
593
|
+
"@kici-dev/sdk": "latest"
|
|
594
594
|
},
|
|
595
595
|
"kici": {
|
|
596
596
|
"sdkPath": "../../packages/sdk"
|
|
@@ -602,7 +602,7 @@ The `sdkPath` field tells the compiler where to resolve TypeScript path mappings
|
|
|
602
602
|
|
|
603
603
|
### KICI_DEV environment variable
|
|
604
604
|
|
|
605
|
-
Set `KICI_DEV=true` to
|
|
605
|
+
Set `KICI_DEV=true` to pin the SDK to the `latest` dist-tag in generated files, which resolves prerelease builds from a local Verdaccio registry:
|
|
606
606
|
|
|
607
607
|
```bash
|
|
608
608
|
KICI_DEV=true npx kici init
|
|
@@ -847,7 +847,7 @@ Each guide is split into two parts. **Part 1** gets you a green run against your
|
|
|
847
847
|
|
|
848
848
|
## Bring a coding agent
|
|
849
849
|
|
|
850
|
-
Workflows are TypeScript, so a coding agent can write them. KiCI ships its docs in a form an agent reads directly: point yours at [llms.txt](https://kici.dev/
|
|
850
|
+
Workflows are TypeScript, so a coding agent can write them. KiCI ships its docs in a form an agent reads directly: point yours at [llms.txt](https://kici.dev/llms.txt), or pipe a task bundle straight into its context with `kici docs llm` (`kici docs llm sdk` for the authoring API, `kici docs llm patterns` for recipes). An agent briefed that way can scaffold your first workflow, compile it, and read the failure when it breaks.
|
|
851
851
|
|
|
852
852
|
If your agent finds that these docs promise something KiCI does not do, that is worth telling us — `kici feedback` prints how to report it, and [Reporting a discrepancy](https://docs.kici.dev/user/reporting-discrepancies/) is the full guide.
|
|
853
853
|
|
|
@@ -893,8 +893,10 @@ promised. You can report that.
|
|
|
893
893
|
A human reader can use this page too. The rules below exist because the tracker
|
|
894
894
|
is public and you are filing under someone else's GitHub identity.
|
|
895
895
|
|
|
896
|
-
Run `kici feedback` to print this contract in your terminal,
|
|
897
|
-
`kici feedback --json` to read it as structured data
|
|
896
|
+
Run `kici feedback` to print this contract in your terminal,
|
|
897
|
+
`kici feedback --json` to read it as structured data, or
|
|
898
|
+
`kici feedback --draft <file.json> --open` to turn a draft into the prefilled form
|
|
899
|
+
and open it.
|
|
898
900
|
|
|
899
901
|
## The rule, in one sentence
|
|
900
902
|
|
|
@@ -969,19 +971,20 @@ approval rule.
|
|
|
969
971
|
|
|
970
972
|
## What the report must carry
|
|
971
973
|
|
|
972
|
-
|
|
974
|
+
A report missing any of these fields cannot be acted on:
|
|
973
975
|
|
|
974
976
|
| Field | What it holds |
|
|
975
977
|
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
976
978
|
| What the docs or CLI advertise | The exact claim, quoted, plus its source: a docs URL or the command whose `--help` says it. |
|
|
977
979
|
| What actually happened | The real output or behaviour, quoted, with any error text. |
|
|
978
980
|
| Minimal reproduction, including setup | Every step from an empty directory: the setup commands, a minimal synthetic workflow, and the exact command you ran. |
|
|
979
|
-
|
|
|
981
|
+
| KiCI version | Output of `kici --version`. |
|
|
982
|
+
| Environment | Node version and OS. |
|
|
980
983
|
| Why this is a discrepancy | One or two sentences ruling out the likely misreads — why the docs cannot be read to match what you observed. |
|
|
981
984
|
|
|
982
985
|
The last field is the one agents skip, and it is the one that makes a report
|
|
983
986
|
usable. "The docs say `--foo` exists and it does not" is a claim. "`--foo` is
|
|
984
|
-
documented at <url>, and `kici bar --help` on
|
|
987
|
+
documented at <url>, and `kici bar --help` on the version I ran lists no such flag" is a
|
|
985
988
|
finding someone can act on in one pass.
|
|
986
989
|
|
|
987
990
|
Include setup steps whenever your reproduction needed any. A reproduction that
|
|
@@ -990,22 +993,36 @@ starts from state a maintainer cannot recreate is not a reproduction.
|
|
|
990
993
|
## Filing it
|
|
991
994
|
|
|
992
995
|
The tracker is [kici-dev/kici-public](https://github.com/kici-dev/kici-public),
|
|
993
|
-
and the form is **Agent report
|
|
996
|
+
and the form is **Agent report** (`agent_report.yml`). It is a GitHub issue
|
|
997
|
+
form, which only the browser can submit — `gh issue create` cannot fill it.
|
|
998
|
+
So you draft, and the person files.
|
|
994
999
|
|
|
995
|
-
|
|
996
|
-
|
|
1000
|
+
Write the draft as JSON, one key per field of the form (`kici feedback --json`
|
|
1001
|
+
lists them under `draftFields`):
|
|
1002
|
+
|
|
1003
|
+
```json
|
|
1004
|
+
{
|
|
1005
|
+
"title": "kici compile --check writes a lock file",
|
|
1006
|
+
"advertised": "https://docs.kici.dev/user/cli-reference/ says: `kici compile --check` type-checks without writing a lock file.",
|
|
1007
|
+
"observed": ".kici/kici.lock.json was written; mtime changed.",
|
|
1008
|
+
"reproduction": "$ mkdir /tmp/repro && cd /tmp/repro\n$ kici init\n$ kici compile --check",
|
|
1009
|
+
"version": "0.8.0",
|
|
1010
|
+
"environment": "Node 24.13.0, Ubuntu 24.04",
|
|
1011
|
+
"justification": "The sentence names --check as the no-write mode; no reading of it allows a write."
|
|
1012
|
+
}
|
|
997
1013
|
```
|
|
998
1014
|
|
|
999
|
-
|
|
1000
|
-
the whole body, not a summary of it. File only after they say yes:
|
|
1015
|
+
Then build the prefilled form:
|
|
1001
1016
|
|
|
1002
1017
|
```bash
|
|
1003
|
-
|
|
1018
|
+
kici feedback --draft draft.json --open
|
|
1004
1019
|
```
|
|
1005
1020
|
|
|
1006
|
-
The
|
|
1007
|
-
|
|
1008
|
-
|
|
1021
|
+
The command prints the form URL with every field filled in and, with
|
|
1022
|
+
`--open`, opens it. It reaches no network and files nothing. Show the person
|
|
1023
|
+
you are working with the whole draft — not a summary of it. They open the
|
|
1024
|
+
URL, read the prefilled form, tick the review boxes, and submit. File only
|
|
1025
|
+
after they say yes.
|
|
1009
1026
|
|
|
1010
1027
|
If they say no, that is the end of it. Do not re-file the same finding in
|
|
1011
1028
|
another form, and do not open it as a question instead.
|
|
@@ -3673,9 +3690,8 @@ The throw is a `ChangedFilesUnavailableError` (exported from `@kici-dev/sdk`, ca
|
|
|
3673
3690
|
### evaluateRules(rules, context, label, onRuleResult?)
|
|
3674
3691
|
|
|
3675
3692
|
The agent calls this on your behalf. A workflow does not call it. It lives on
|
|
3676
|
-
`@kici-dev/sdk/internal`, outside semver, and
|
|
3677
|
-
|
|
3678
|
-
because its return shape is what a rule's outcome looks like in the run log.
|
|
3693
|
+
`@kici-dev/sdk/internal`, outside semver, and is not exported from the root barrel. It is
|
|
3694
|
+
described here because its return shape is what a rule's outcome looks like in the run log.
|
|
3679
3695
|
|
|
3680
3696
|
Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
|
|
3681
3697
|
|
|
@@ -3928,9 +3944,8 @@ isDynamicFunction(matrix); // true if async function
|
|
|
3928
3944
|
### Matrix expansion utilities
|
|
3929
3945
|
|
|
3930
3946
|
The agent expands a matrix for you. A workflow does not call these. They live on
|
|
3931
|
-
`@kici-dev/sdk/internal`, outside semver, and
|
|
3932
|
-
|
|
3933
|
-
here because they define the combinations a matrix job actually produces.
|
|
3947
|
+
`@kici-dev/sdk/internal`, outside semver, and are not exported from the root barrel. They are
|
|
3948
|
+
described here because they define the combinations a matrix job actually produces.
|
|
3934
3949
|
|
|
3935
3950
|
```typescript
|
|
3936
3951
|
import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk/internal';
|
|
@@ -4527,7 +4542,7 @@ Create a commit status trigger. Returns a frozen `StatusTriggerConfig`.
|
|
|
4527
4542
|
function status(config?: StatusConfigInput): StatusTriggerConfig;
|
|
4528
4543
|
```
|
|
4529
4544
|
|
|
4530
|
-
**Config options:** `contexts` (
|
|
4545
|
+
**Config options:** `contexts` (glob patterns like 'ci/\*'), `states` (error/failure/pending/success), `description`
|
|
4531
4546
|
|
|
4532
4547
|
```typescript
|
|
4533
4548
|
status(); // Any status
|
|
@@ -5153,11 +5168,8 @@ a `.result` proxy reads, build the step context your workflow body receives, eva
|
|
|
5153
5168
|
rules, and expand its matrix. The agent drives all of it on your behalf.
|
|
5154
5169
|
|
|
5155
5170
|
It is **not covered by semver** and may change shape in any release. Do not import it from
|
|
5156
|
-
a workflow. Everything a workflow author needs is on the root entry point above
|
|
5157
|
-
|
|
5158
|
-
Those same symbols are also still exported from the root barrel, marked `@deprecated`, so
|
|
5159
|
-
an older SDK in a repository keeps working. They are removed from the root at v1.0.0 — see
|
|
5160
|
-
[deprecations](https://docs.kici.dev/user/deprecations/).
|
|
5171
|
+
a workflow. Everything a workflow author needs is on the root entry point above, and none
|
|
5172
|
+
of the runtime contract is exported there.
|
|
5161
5173
|
|
|
5162
5174
|
## See also
|
|
5163
5175
|
|
|
@@ -6502,6 +6514,17 @@ interface StepContext<TInputs = Record<string, unknown>> {
|
|
|
6502
6514
|
provider?: string;
|
|
6503
6515
|
/** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */
|
|
6504
6516
|
isTestRun: boolean;
|
|
6517
|
+
/** The job's own checked-out repository (present for every job that checks out); `withWrite` opens a write window for it — see [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/) */
|
|
6518
|
+
repo?: {
|
|
6519
|
+
identifier: string;
|
|
6520
|
+
path: string;
|
|
6521
|
+
ref?: string;
|
|
6522
|
+
sha?: string;
|
|
6523
|
+
withWrite(
|
|
6524
|
+
opts: { permissions?: Record<string, string>; credential?: string },
|
|
6525
|
+
fn: () => Promise<void>,
|
|
6526
|
+
): Promise<void>;
|
|
6527
|
+
};
|
|
6505
6528
|
/** Registering repo of a global workflow (undefined for non-global workflows) */
|
|
6506
6529
|
workflowRepo?: RepoInfo;
|
|
6507
6530
|
/** Repo where the triggering event occurred (undefined for non-global workflows) */
|
|
@@ -6702,13 +6725,15 @@ const deploy = job('deploy', {
|
|
|
6702
6725
|
|
|
6703
6726
|
Request a short-lived OIDC ID token for the current job, bound to an `audience`. The token is a signed JWT whose identity claims (`repository`, `ref`, `sha`, `kici_run_id`, `kici_job_id`) are derived by your orchestrator from the run context — a step cannot spoof them. Use it to authenticate the build to an external service that trusts the orchestrator's OIDC issuer (for example, when generating build provenance).
|
|
6704
6727
|
|
|
6705
|
-
The token also carries the event context a cloud trust policy needs to tell a fork pull request from a trusted push — `is_fork`, `head_repository`, `trust_tier`, `event_name`, and a pull-request-specific `sub`. See [ID-token claims and cloud trust policies](https://docs.kici.dev/user/
|
|
6728
|
+
The token also carries the event context a cloud trust policy needs to tell a fork pull request from a trusted push — `is_fork`, `head_repository`, `trust_tier`, `event_name`, and a pull-request-specific `sub`. See [ID-token claims and cloud trust policies](https://docs.kici.dev/user/oidc/#id-token-claims-and-cloud-trust-policies) for the full claim table and a worked AWS policy.
|
|
6706
6729
|
|
|
6707
6730
|
```typescript
|
|
6708
6731
|
const publish = job('publish', {
|
|
6709
6732
|
steps: [
|
|
6710
6733
|
step('mint', async (ctx) => {
|
|
6711
|
-
const
|
|
6734
|
+
const minted = await ctx.kici.oidc.token({ audience: 'sigstore' });
|
|
6735
|
+
if ('deferred' in minted) throw new Error(`ID token deferred: ${minted.code}`);
|
|
6736
|
+
const { token, expiresIn } = minted;
|
|
6712
6737
|
ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
|
|
6713
6738
|
// Hand `token` to a tool that exchanges it with the trusting service.
|
|
6714
6739
|
}),
|
|
@@ -6718,6 +6743,7 @@ const publish = job('publish', {
|
|
|
6718
6743
|
|
|
6719
6744
|
**Behavior:**
|
|
6720
6745
|
|
|
6746
|
+
- The result is the minted token, or `{ deferred: true, code }` (`unavailable` | `failed`) when the orchestrator could not mint one right now. A step that exchanges the token fails on `deferred`; `ctx.attestProvenance` handles it by freezing the statement for later fulfilment.
|
|
6721
6747
|
- The token is short-lived (about 10 minutes) and scoped to the current run and job.
|
|
6722
6748
|
- The returned token value is automatically masked in step logs.
|
|
6723
6749
|
- The step never holds signing credentials — the orchestrator mints and signs the token on the step's behalf from its own run records.
|
|
@@ -8573,7 +8599,7 @@ Manage the **local dev plane** — the warm, per-user orchestrator (plus its own
|
|
|
8573
8599
|
kici local up [--offline | --connected] # Start, or reuse an already-running plane
|
|
8574
8600
|
kici local status [--json] # Port, pid, PostgreSQL backend, attachment mode, readiness
|
|
8575
8601
|
kici local down # Stop the orchestrator and its PostgreSQL, verifying the port is freed
|
|
8576
|
-
kici local logs # Print the
|
|
8602
|
+
kici local logs # Print the plane log paths and rotation policy
|
|
8577
8603
|
kici local attach # Attach to the Platform (hybrid mode)
|
|
8578
8604
|
kici local detach # Return the plane to offline (independent) mode
|
|
8579
8605
|
kici local trust-root <file> # Export the dev-signed trust root for offline verification
|
|
@@ -8582,7 +8608,7 @@ kici local trust-root <file> # Export the dev-signed trust root fo
|
|
|
8582
8608
|
The plane runs in one of two modes:
|
|
8583
8609
|
|
|
8584
8610
|
- **Independent (offline)** — the default for a plane that has never been attached. Identity tokens and attestations are signed by a local dev key under the clearly non-production issuer `kici-local`.
|
|
8585
|
-
- **Hybrid (attached)** — `kici local attach` mints an org-scoped key with your logged-in credentials and reboots the plane connected to the Platform, so local runs get
|
|
8611
|
+
- **Hybrid (attached)** — `kici local attach` mints an org-scoped key with your logged-in credentials and reboots the plane connected to the Platform, so local runs get identity and attestation signed by the plane's own key under its own issuer, as a deployed orchestrator would. `kici local up` honors a durable attachment record: an attached plane comes back up hybrid, and falls back to offline with a warning when the Platform is unreachable.
|
|
8586
8612
|
|
|
8587
8613
|
`--offline` forces an independent boot without clearing the attachment record (only `detach` clears it); `--connected` requires an attached, reachable Platform and fails otherwise.
|
|
8588
8614
|
|
|
@@ -8590,6 +8616,8 @@ The plane runs in one of two modes:
|
|
|
8590
8616
|
|
|
8591
8617
|
`kici local status` reports a plane whose process is alive but whose readiness probe fails — for example when its PostgreSQL has stopped — as running but not ready, together with its readiness checks, rather than as not running. When the holder is a KiCI plane orchestrator that this config directory did not start — a plane belonging to another `KICI_CONFIG_DIR`, or one whose record here was lost — status names it as such rather than as not ready, since its readiness is never probed, and points at `kici local down`, which does reclaim it. When the port is held by a process that is not a KiCI plane orchestrator, `kici local status` names that holder instead and points at `KICI_LOCAL_ORCH_PORT`, because `kici local down` will not stop it.
|
|
8592
8618
|
|
|
8619
|
+
The plane writes its orchestrator log to `orchestrator.log` in its state directory and, when it runs embedded PostgreSQL, the PostgreSQL log to `orchestrator.log.pg` beside it. `kici local logs` prints their location. Each is rotated to a `.1` sibling when it reaches 50 MB, at the next plane start — the plane keeps the current log and one previous generation, so neither can grow without bound.
|
|
8620
|
+
|
|
8593
8621
|
Pass `--json` for machine-readable output. It prints one object and exits 0 for
|
|
8594
8622
|
every state, including when the plane is stopped — the state is in the payload,
|
|
8595
8623
|
not the exit code:
|
|
@@ -8748,7 +8776,7 @@ If multiple tools are detected, you are prompted to choose.
|
|
|
8748
8776
|
Open the KiCI documentation site in the default browser. With the `llm` subcommand, print the LLM-friendly documentation bundle that ships with `@kici-dev/compiler` — pipe it into a coding agent's context buffer to brief the agent on authoring conventions without an internet round-trip.
|
|
8749
8777
|
|
|
8750
8778
|
```bash
|
|
8751
|
-
kici docs # open https://kici.dev/
|
|
8779
|
+
kici docs # open https://docs.kici.dev/
|
|
8752
8780
|
kici docs --no-open # print the URL instead of opening a browser
|
|
8753
8781
|
kici docs llm # print the llms.txt index (a router over the task bundles)
|
|
8754
8782
|
kici docs llm sdk # print the SDK task bundle
|
|
@@ -8882,7 +8910,7 @@ Synopsis: `kici local down`
|
|
|
8882
8910
|
|
|
8883
8911
|
### `kici local logs`
|
|
8884
8912
|
|
|
8885
|
-
Print the local dev plane
|
|
8913
|
+
Print the local dev plane log paths and rotation policy
|
|
8886
8914
|
|
|
8887
8915
|
Synopsis: `kici local logs`
|
|
8888
8916
|
|
|
@@ -9030,9 +9058,7 @@ behalf). The token is printed once — save it now; it cannot be retrieved later
|
|
|
9030
9058
|
|
|
9031
9059
|
**Option B — an agent org API key.** Create one from the dashboard's
|
|
9032
9060
|
**Settings → API keys** tab: set the key's kind to **Agent** and give it an
|
|
9033
|
-
agent name (the agent label).
|
|
9034
|
-
`kici-platform-admin user api-key create --org <id> --agent --agent-label <label>`.
|
|
9035
|
-
Reach for an org agent key when the agent should act as a shared service account
|
|
9061
|
+
agent name (the agent label). Reach for an org agent key when the agent should act as a shared service account
|
|
9036
9062
|
rather than as a single user — for example, a long-lived CI bot that outlives any
|
|
9037
9063
|
individual's membership.
|
|
9038
9064
|
|
|
@@ -9747,7 +9773,7 @@ In interactive mode (TTY), `kici init` prompts you to:
|
|
|
9747
9773
|
|
|
9748
9774
|
**Standalone vs workspace integration:** by default `kici init` scaffolds a self-contained `.kici/` with its own `package.json`. When run inside a pnpm, npm, or yarn workspace, it offers an **integrate** option (or pass `--workspace`): `.kici/` joins the workspace, `@kici-dev/sdk` is added to your workspace-root `package.json`, and your workflows can `import` your other workspace packages (e.g. shared build or deploy utilities). In this mode there is no `.kici/package.json` — the workspace root manages dependencies, and the root install resolves the SDK. Your workflows resolve sibling packages through the workspace-root `node_modules`: under npm and yarn every workspace member is hoisted there automatically, while pnpm links only your root's declared dependencies, so under pnpm add the package you want to import to your workspace-root `dependencies` (this is how KiCI's own repository imports its packages from workflows). Pass `--standalone` to force the self-contained layout even inside a workspace. In CI / non-interactive runs the default is standalone; use `--workspace` to opt in explicitly. `--workspace` and `--standalone` are mutually exclusive, and `--workspace` errors if no workspace is found at or above the current directory.
|
|
9749
9775
|
|
|
9750
|
-
**Development mode:** When `KICI_DEV=true` or `package.json` has `"kici": { "development": true }`, the generated `package.json`
|
|
9776
|
+
**Development mode:** When `KICI_DEV=true` or `package.json` has `"kici": { "development": true }`, the generated `package.json` pins `@kici-dev/sdk` to the `latest` dist-tag so npm resolves Verdaccio's newest prerelease build.
|
|
9751
9777
|
|
|
9752
9778
|
### kici org
|
|
9753
9779
|
|
|
@@ -10184,10 +10210,11 @@ kici verify-attestation --bundle ./app.tgz.kici.json \
|
|
|
10184
10210
|
token was minted relative to the build. A normal attestation prints no marker
|
|
10185
10211
|
(the token was minted live). A **deferred** attestation prints an `ATTESTATION:
|
|
10186
10212
|
deferred` line — the build facts were sealed at build time and the token was
|
|
10187
|
-
minted later,
|
|
10188
|
-
by its hash. An **offline-backfill** attestation prints an
|
|
10189
|
-
offline-backfill` line — the run was ingested while the platform
|
|
10190
|
-
run/job
|
|
10213
|
+
minted later, once the orchestrator's signing key was available again, bound to
|
|
10214
|
+
the frozen statement by its hash. An **offline-backfill** attestation prints an
|
|
10215
|
+
`ATTESTATION: offline-backfill` line — the run was ingested while the platform
|
|
10216
|
+
was down, so its run/job records were replayed to the platform before the token
|
|
10217
|
+
was minted. Both still verify
|
|
10191
10218
|
(PASS); the marker discloses the temporal gap, and the organization id remains
|
|
10192
10219
|
the authoritative anchor.
|
|
10193
10220
|
|
|
@@ -10346,6 +10373,9 @@ kici feedback --open
|
|
|
10346
10373
|
|
|
10347
10374
|
# Read the same contract as structured data
|
|
10348
10375
|
kici feedback --json
|
|
10376
|
+
|
|
10377
|
+
# Turn a JSON draft (keys: draftFields from --json) into the prefilled form URL
|
|
10378
|
+
kici feedback --draft draft.json --open
|
|
10349
10379
|
```
|
|
10350
10380
|
|
|
10351
10381
|
`--json` exists for coding agents: KiCI is built to be driven by an LLM, and
|
|
@@ -10396,10 +10426,11 @@ Synopsis: `kici feedback [options]`
|
|
|
10396
10426
|
|
|
10397
10427
|
**Options**
|
|
10398
10428
|
|
|
10399
|
-
| Option
|
|
10400
|
-
|
|
|
10401
|
-
| `--open`
|
|
10402
|
-
| `--json`
|
|
10429
|
+
| Option | Default | Description |
|
|
10430
|
+
| ---------------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
10431
|
+
| `--open` | | Open the prefilled issue form in the default browser |
|
|
10432
|
+
| `--json` | | Emit the reporting contract as JSON |
|
|
10433
|
+
| `--draft <file>` | | Build the prefilled issue-form URL from a JSON draft keyed by field id (with --open, open it) |
|
|
10403
10434
|
|
|
10404
10435
|
### `kici notifications`
|
|
10405
10436
|
|
|
@@ -10693,6 +10724,18 @@ Secrets are always sourced from your real `.kici/` directory, not from the isola
|
|
|
10693
10724
|
|
|
10694
10725
|
Pass `--in-place` to run against the real working directory instead — useful when you explicitly want in-tree execution. `--in-place` requires no git repository; the default isolated mode does, and fails with an actionable error pointing at `--in-place` when the directory is not a git repository.
|
|
10695
10726
|
|
|
10727
|
+
**If the trigger times out:**
|
|
10728
|
+
|
|
10729
|
+
The command waits up to 60 seconds for the local dev plane to create the run. If nothing appears in that window it stops and names the cause it read back from the plane:
|
|
10730
|
+
|
|
10731
|
+
| Message | What it means |
|
|
10732
|
+
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10733
|
+
| `plane is not leader yet (election grace period)` | The plane had not finished electing itself. A single-machine plane elects within seconds; if it does not, read the plane log (`kici local logs`). |
|
|
10734
|
+
| `no kici.lock.json at <commit> in <path>` | The plane resolved no lock file for the commit the run packed, so nothing matched. Commit `.kici/kici.lock.json`, or make sure it is present in the working tree. |
|
|
10735
|
+
| `the plane recorded delivery <id> as "<status>"` | The plane processed the trigger and wrote down a terminal status for it without creating a run. The status names the stage that stopped. |
|
|
10736
|
+
| `no run appeared for delivery <id> — see <log>` | Neither the plane's cluster state nor its delivery record explained the timeout. The named log is the next place to look. |
|
|
10737
|
+
| `no run appeared for this trigger — the plane never accepted the webhook` | Every webhook POST came back without a delivery id, so the plane never queued the trigger. Check the plane is up (`kici local status`), then read the named log. |
|
|
10738
|
+
|
|
10696
10739
|
**Examples:**
|
|
10697
10740
|
|
|
10698
10741
|
```bash
|
|
@@ -11922,13 +11965,12 @@ Hold a job whose run came from a fork:
|
|
|
11922
11965
|
Minimum trust: trusted
|
|
11923
11966
|
```
|
|
11924
11967
|
|
|
11925
|
-
| Value | Effect
|
|
11926
|
-
| --------- |
|
|
11927
|
-
| `trusted` | Holds a run whose ref came from a fork
|
|
11928
|
-
|
|
|
11929
|
-
| (unset) | No trust-based gating |
|
|
11968
|
+
| Value | Effect |
|
|
11969
|
+
| --------- | -------------------------------------- |
|
|
11970
|
+
| `trusted` | Holds a run whose ref came from a fork |
|
|
11971
|
+
| (unset) | No trust-based gating |
|
|
11930
11972
|
|
|
11931
|
-
|
|
11973
|
+
Trust comes from the git ref, and that judgement has two answers: a ref in your repository is `trusted`, a ref from a fork is `unknown`.
|
|
11932
11974
|
|
|
11933
11975
|
When the gate holds a job, it enters the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
|
|
11934
11976
|
|
|
@@ -12083,6 +12125,439 @@ This guide is split across the following pages:
|
|
|
12083
12125
|
|
|
12084
12126
|
---
|
|
12085
12127
|
|
|
12128
|
+
## Workload identity with OIDC
|
|
12129
|
+
|
|
12130
|
+
Source: https://docs.kici.dev/user/oidc/
|
|
12131
|
+
|
|
12132
|
+
A job step can prove **which run it is** to an outside service without a stored
|
|
12133
|
+
secret. The orchestrator mints a short-lived OIDC identity token for the step,
|
|
12134
|
+
signs it with its own key, and the service verifies it against the
|
|
12135
|
+
orchestrator's public keys. Your repository holds no long-lived credential for
|
|
12136
|
+
that service; the exchanged credential exists only inside the step.
|
|
12137
|
+
|
|
12138
|
+
The orchestrator is the OIDC issuer. It publishes the discovery document and
|
|
12139
|
+
the key set under `KICI_ORCHESTRATOR_PROVENANCE_ISSUER`, and the service you
|
|
12140
|
+
exchange with must be able to reach that base URL — see
|
|
12141
|
+
[network requirements](https://docs.kici.dev/operator/network-requirements/) and
|
|
12142
|
+
[signing keys](https://docs.kici.dev/operator/orchestrator/signing-keys/) for the operator side.
|
|
12143
|
+
|
|
12144
|
+
## Requesting an identity token
|
|
12145
|
+
|
|
12146
|
+
Call `ctx.kici.oidc.token({ audience })` from a step when you need the identity
|
|
12147
|
+
token for a tool that trusts your orchestrator:
|
|
12148
|
+
|
|
12149
|
+
```typescript
|
|
12150
|
+
step('mint', async (ctx) => {
|
|
12151
|
+
const minted = await ctx.kici.oidc.token({ audience: 'sigstore' });
|
|
12152
|
+
if ('deferred' in minted) throw new Error(`ID token deferred: ${minted.code}`);
|
|
12153
|
+
const { token, expiresIn } = minted;
|
|
12154
|
+
ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
|
|
12155
|
+
// Hand `token` to a tool that exchanges it with a service trusting the issuer.
|
|
12156
|
+
});
|
|
12157
|
+
```
|
|
12158
|
+
|
|
12159
|
+
The result is either the minted token or `{ deferred: true, code }` when the
|
|
12160
|
+
orchestrator could not mint one right now (`unavailable` or `failed`). An
|
|
12161
|
+
exchange needs a live token, so a step that exchanges it fails on `deferred`;
|
|
12162
|
+
[`ctx.attestProvenance`](https://docs.kici.dev/user/provenance/) handles that case for you by freezing
|
|
12163
|
+
the statement and fulfilling it later. The token is a short-lived (about 10
|
|
12164
|
+
minutes) signed JWT scoped to the current run and job. Its identity claims
|
|
12165
|
+
(`repository`, `ref`, `sha`, `kici_run_id`, `kici_job_id`) are derived by the
|
|
12166
|
+
orchestrator from the run context, so a step cannot spoof them. The returned
|
|
12167
|
+
token value is automatically masked in step logs, and the step never holds
|
|
12168
|
+
signing credentials — the orchestrator mints and signs the token on the step's
|
|
12169
|
+
behalf from its own run records. It is only available inside a running job step.
|
|
12170
|
+
|
|
12171
|
+
## Exchanging the token with an external service
|
|
12172
|
+
|
|
12173
|
+
Any service that federates with a generic OIDC issuer can trust your
|
|
12174
|
+
orchestrator directly. The service fetches your issuer's discovery document
|
|
12175
|
+
(`<issuer>/.well-known/openid-configuration`) and public keys, so the issuer
|
|
12176
|
+
base URL must be reachable from that service.
|
|
12177
|
+
|
|
12178
|
+
The pattern is the same everywhere: request a token with the audience the
|
|
12179
|
+
service expects, exchange it for the service's own short-lived credential, and
|
|
12180
|
+
use that credential inside the step. The workflows below are the ones KiCI's
|
|
12181
|
+
own test suite runs against a real Cloudsmith organization and a real AWS
|
|
12182
|
+
account. Each publishes a probe, reads it back, and deletes it. Replace the
|
|
12183
|
+
probe with your real publish or upload.
|
|
12184
|
+
|
|
12185
|
+
Only the ID token is masked in step logs automatically. A credential you
|
|
12186
|
+
exchange it for is an ordinary string, so do not log it. Hand it to child
|
|
12187
|
+
processes through a scratch file or the environment, never on a command line.
|
|
12188
|
+
|
|
12189
|
+
### Cloudsmith
|
|
12190
|
+
|
|
12191
|
+
Cloudsmith exchanges any trusted issuer's token for a Cloudsmith API token
|
|
12192
|
+
through `POST https://api.cloudsmith.io/openid/<org>/`. The exchanged
|
|
12193
|
+
credential works as the Cloudsmith API key and as an npm auth token:
|
|
12194
|
+
|
|
12195
|
+
<!-- BEGIN GENERATED: oidc-cloudsmith-workflow (do not edit; run the doc generator) -->
|
|
12196
|
+
|
|
12197
|
+
```typescript
|
|
12198
|
+
import { writeFile } from 'node:fs/promises';
|
|
12199
|
+
import { join } from 'node:path';
|
|
12200
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
12201
|
+
|
|
12202
|
+
// Exchanges the job's OIDC ID token with Cloudsmith, publishes a probe npm
|
|
12203
|
+
// package, reads it back through the Cloudsmith API and deletes it. Every
|
|
12204
|
+
// Cloudsmith call is made with the exchanged token; nothing here is a secret
|
|
12205
|
+
// the repo stores.
|
|
12206
|
+
const ORG = 'my-org';
|
|
12207
|
+
const REPO = 'my-repo';
|
|
12208
|
+
const REGISTRY = `https://npm.cloudsmith.io/${ORG}/${REPO}/`;
|
|
12209
|
+
const PACKAGES_API = `https://api.cloudsmith.io/v1/packages/${ORG}/${REPO}/`;
|
|
12210
|
+
const PREFIX = '[cloudsmith-oidc]';
|
|
12211
|
+
|
|
12212
|
+
function decodeJwtPart(token: string, index: number): Record<string, unknown> {
|
|
12213
|
+
return JSON.parse(Buffer.from(token.split('.')[index], 'base64url').toString('utf8'));
|
|
12214
|
+
}
|
|
12215
|
+
|
|
12216
|
+
export default workflow('publish', {
|
|
12217
|
+
on: push({ branches: ['main'] }),
|
|
12218
|
+
jobs: [
|
|
12219
|
+
job('publish', {
|
|
12220
|
+
runsOn: 'container',
|
|
12221
|
+
steps: [
|
|
12222
|
+
step('exchange-and-publish', async (ctx) => {
|
|
12223
|
+
// 1. Mint. Log the signing algorithm, key id and subject: when the
|
|
12224
|
+
// provider rejects the token, this line says which claim to compare
|
|
12225
|
+
// against the provider's required claims.
|
|
12226
|
+
const minted = await ctx.kici.oidc.token({ audience: 'cloudsmith' });
|
|
12227
|
+
// A deferred mint is a transient orchestrator-side failure; an exchange
|
|
12228
|
+
// needs a live token, so the step fails rather than freezing a statement.
|
|
12229
|
+
if ('deferred' in minted) throw new Error(`ID token mint deferred: ${minted.code}`);
|
|
12230
|
+
const { token } = minted;
|
|
12231
|
+
const header = decodeJwtPart(token, 0);
|
|
12232
|
+
const payload = decodeJwtPart(token, 1);
|
|
12233
|
+
ctx.log.info(`${PREFIX} alg=${header.alg} kid=${header.kid} sub=${payload.sub}`);
|
|
12234
|
+
|
|
12235
|
+
// 2. Exchange.
|
|
12236
|
+
const exchange = await fetch(`https://api.cloudsmith.io/openid/${ORG}/`, {
|
|
12237
|
+
method: 'POST',
|
|
12238
|
+
headers: { 'content-type': 'application/json' },
|
|
12239
|
+
body: JSON.stringify({ oidc_token: token, service_slug: 'ci-publisher' }),
|
|
12240
|
+
});
|
|
12241
|
+
if (!exchange.ok) {
|
|
12242
|
+
const body = (await exchange.text()).split(token).join('<id-token>');
|
|
12243
|
+
ctx.log.error(`${PREFIX} exchange=failed status=${exchange.status} body=${body}`);
|
|
12244
|
+
throw new Error(`Cloudsmith token exchange failed: ${exchange.status}`);
|
|
12245
|
+
}
|
|
12246
|
+
const { token: csToken } = (await exchange.json()) as { token: string };
|
|
12247
|
+
ctx.log.info(`${PREFIX} exchange=ok`);
|
|
12248
|
+
|
|
12249
|
+
// 3. Publish a unique probe version. The npmrc carrying the exchanged
|
|
12250
|
+
// token is a scratch file the job removes; it never reaches argv.
|
|
12251
|
+
// The run-id segment is prefixed with a letter: a prerelease identifier
|
|
12252
|
+
// made only of digits is numeric under semver and may not start with
|
|
12253
|
+
// 0, so a run id such as 01234567 would make npm refuse the version.
|
|
12254
|
+
const runIdShort = String(payload.kici_run_id).slice(0, 8);
|
|
12255
|
+
const version = `0.0.0-e2e.r${runIdShort}.${Math.floor(Date.now() / 1000)}`;
|
|
12256
|
+
const name = '@kici-e2e/oidc-probe';
|
|
12257
|
+
const pkgDir = await ctx.mktemp('oidc-probe');
|
|
12258
|
+
await writeFile(
|
|
12259
|
+
join(pkgDir.path, 'package.json'),
|
|
12260
|
+
JSON.stringify({ name, version, license: 'MIT', main: 'index.js' }, null, 2),
|
|
12261
|
+
);
|
|
12262
|
+
await writeFile(join(pkgDir.path, 'index.js'), 'module.exports = "kici oidc probe";\n');
|
|
12263
|
+
const npmrc = await ctx.mktempFile('cloudsmith-npmrc');
|
|
12264
|
+
await writeFile(
|
|
12265
|
+
npmrc.path,
|
|
12266
|
+
`//npm.cloudsmith.io/${ORG}/${REPO}/:_authToken=${csToken}\n`,
|
|
12267
|
+
);
|
|
12268
|
+
await ctx.$({
|
|
12269
|
+
cwd: pkgDir.path,
|
|
12270
|
+
env: { ...process.env, NPM_CONFIG_USERCONFIG: npmrc.path },
|
|
12271
|
+
})`npm publish --registry ${REGISTRY} --tag e2e`;
|
|
12272
|
+
|
|
12273
|
+
// 4. Read it back through the API with the same token, then delete.
|
|
12274
|
+
// The version is unique per run, so it is the whole query.
|
|
12275
|
+
const authHeaders = { 'X-Api-Key': `Bearer ${csToken}` };
|
|
12276
|
+
const query = encodeURIComponent(`version:${version}`);
|
|
12277
|
+
let slug: string | undefined;
|
|
12278
|
+
const deadline = Date.now() + 120_000;
|
|
12279
|
+
while (Date.now() < deadline && !slug) {
|
|
12280
|
+
const list = await fetch(`${PACKAGES_API}?query=${query}`, { headers: authHeaders });
|
|
12281
|
+
if (list.ok) {
|
|
12282
|
+
const rows = (await list.json()) as Array<{
|
|
12283
|
+
slug: string;
|
|
12284
|
+
is_sync_completed: boolean;
|
|
12285
|
+
}>;
|
|
12286
|
+
const done = rows.find((r) => r.is_sync_completed);
|
|
12287
|
+
if (done) slug = done.slug;
|
|
12288
|
+
}
|
|
12289
|
+
if (!slug) await new Promise((r) => setTimeout(r, 5_000));
|
|
12290
|
+
}
|
|
12291
|
+
if (!slug) throw new Error(`${name}@${version} never finished syncing in Cloudsmith`);
|
|
12292
|
+
const del = await fetch(`${PACKAGES_API}${slug}/`, {
|
|
12293
|
+
method: 'DELETE',
|
|
12294
|
+
headers: authHeaders,
|
|
12295
|
+
});
|
|
12296
|
+
if (!del.ok) throw new Error(`Cloudsmith delete failed: ${del.status}`);
|
|
12297
|
+
ctx.log.info(`${PREFIX} verdict=ok package=${name}@${version} deleted=true`);
|
|
12298
|
+
}),
|
|
12299
|
+
],
|
|
12300
|
+
}),
|
|
12301
|
+
],
|
|
12302
|
+
});
|
|
12303
|
+
```
|
|
12304
|
+
|
|
12305
|
+
<!-- END GENERATED: oidc-cloudsmith-workflow -->
|
|
12306
|
+
|
|
12307
|
+
On the Cloudsmith side, add an OpenID Connect provider under your organization's
|
|
12308
|
+
authentication settings. The **provider URL** is your orchestrator's issuer
|
|
12309
|
+
(`KICI_ORCHESTRATOR_PROVENANCE_ISSUER`), and the **service account** is the one
|
|
12310
|
+
named by `service_slug`. The **required claims** pin which runs may
|
|
12311
|
+
authenticate. Pin `sub` to the build identity; for production add the fork
|
|
12312
|
+
conditions from the [claim table](https://docs.kici.dev/user/oidc/#the-claim-set) below so a fork pull request
|
|
12313
|
+
cannot exchange a token:
|
|
12314
|
+
|
|
12315
|
+
```
|
|
12316
|
+
sub: repo:acme/app:ref:main:workflow:publish
|
|
12317
|
+
is_fork: false
|
|
12318
|
+
head_repository: acme/app
|
|
12319
|
+
```
|
|
12320
|
+
|
|
12321
|
+
### AWS
|
|
12322
|
+
|
|
12323
|
+
AWS exchanges the token through `AssumeRoleWithWebIdentity`, which needs no AWS
|
|
12324
|
+
credential of its own. The step imports `@aws-sdk/client-sts` and
|
|
12325
|
+
`@aws-sdk/client-s3`; declare both under `dependencies` in `.kici/package.json`
|
|
12326
|
+
so the compiled workflow can load them.
|
|
12327
|
+
|
|
12328
|
+
<!-- BEGIN GENERATED: oidc-aws-workflow (do not edit; run the doc generator) -->
|
|
12329
|
+
|
|
12330
|
+
```typescript
|
|
12331
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
12332
|
+
import { STSClient, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
|
|
12333
|
+
import {
|
|
12334
|
+
S3Client,
|
|
12335
|
+
PutObjectCommand,
|
|
12336
|
+
GetObjectCommand,
|
|
12337
|
+
DeleteObjectCommand,
|
|
12338
|
+
ListObjectsV2Command,
|
|
12339
|
+
} from '@aws-sdk/client-s3';
|
|
12340
|
+
|
|
12341
|
+
// Exchanges the job's OIDC ID token with AWS STS for role credentials, writes
|
|
12342
|
+
// one probe object to S3, reads it back and deletes it. Nothing here is a
|
|
12343
|
+
// secret the repo stores; the credentials exist only inside this step.
|
|
12344
|
+
const REGION = 'eu-central-1';
|
|
12345
|
+
const ROLE_ARN = 'arn:aws:iam::123456789012:role/ci-uploader';
|
|
12346
|
+
const BUCKET = 'my-artifacts';
|
|
12347
|
+
const PREFIX = '[aws-oidc]';
|
|
12348
|
+
|
|
12349
|
+
function decodeJwtPart(token: string, index: number): Record<string, unknown> {
|
|
12350
|
+
return JSON.parse(Buffer.from(token.split('.')[index], 'base64url').toString('utf8'));
|
|
12351
|
+
}
|
|
12352
|
+
|
|
12353
|
+
export default workflow('upload', {
|
|
12354
|
+
on: push({ branches: ['main'] }),
|
|
12355
|
+
jobs: [
|
|
12356
|
+
job('upload', {
|
|
12357
|
+
runsOn: 'container',
|
|
12358
|
+
steps: [
|
|
12359
|
+
step('assume-and-put', async (ctx) => {
|
|
12360
|
+
// 1. Mint. Log the signing algorithm, key id, subject and audience:
|
|
12361
|
+
// when STS rejects the token, this line says which claim to compare
|
|
12362
|
+
// against the role's trust policy.
|
|
12363
|
+
const minted = await ctx.kici.oidc.token({ audience: 'sts.amazonaws.com' });
|
|
12364
|
+
// A deferred mint is a transient orchestrator-side failure; an exchange
|
|
12365
|
+
// needs a live token, so the step fails rather than freezing a statement.
|
|
12366
|
+
if ('deferred' in minted) throw new Error(`ID token mint deferred: ${minted.code}`);
|
|
12367
|
+
const { token } = minted;
|
|
12368
|
+
const header = decodeJwtPart(token, 0);
|
|
12369
|
+
const payload = decodeJwtPart(token, 1);
|
|
12370
|
+
ctx.log.info(
|
|
12371
|
+
`${PREFIX} alg=${header.alg} kid=${header.kid} sub=${payload.sub} aud=${payload.aud}`,
|
|
12372
|
+
);
|
|
12373
|
+
|
|
12374
|
+
// 2. Exchange. AssumeRoleWithWebIdentity is an unsigned operation, so
|
|
12375
|
+
// the client needs no credentials of its own.
|
|
12376
|
+
const runId = String(payload.kici_run_id);
|
|
12377
|
+
const sts = new STSClient({ region: REGION });
|
|
12378
|
+
let creds;
|
|
12379
|
+
try {
|
|
12380
|
+
const out = await sts.send(
|
|
12381
|
+
new AssumeRoleWithWebIdentityCommand({
|
|
12382
|
+
RoleArn: ROLE_ARN,
|
|
12383
|
+
RoleSessionName: `kici-e2e-${runId.slice(0, 8)}`,
|
|
12384
|
+
WebIdentityToken: token,
|
|
12385
|
+
DurationSeconds: 900,
|
|
12386
|
+
}),
|
|
12387
|
+
);
|
|
12388
|
+
creds = out.Credentials;
|
|
12389
|
+
} catch (err) {
|
|
12390
|
+
const e = err as { name?: string; message?: string };
|
|
12391
|
+
const message = String(e.message ?? err)
|
|
12392
|
+
.split(token)
|
|
12393
|
+
.join('<id-token>');
|
|
12394
|
+
ctx.log.error(`${PREFIX} assume=failed code=${e.name ?? 'unknown'} message=${message}`);
|
|
12395
|
+
throw err;
|
|
12396
|
+
}
|
|
12397
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
|
|
12398
|
+
throw new Error('AssumeRoleWithWebIdentity returned no credentials');
|
|
12399
|
+
}
|
|
12400
|
+
ctx.log.info(`${PREFIX} assume=ok`);
|
|
12401
|
+
|
|
12402
|
+
// 3. Put + get with the role credentials.
|
|
12403
|
+
const s3 = new S3Client({
|
|
12404
|
+
region: REGION,
|
|
12405
|
+
credentials: {
|
|
12406
|
+
accessKeyId: creds.AccessKeyId,
|
|
12407
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
12408
|
+
sessionToken: creds.SessionToken,
|
|
12409
|
+
},
|
|
12410
|
+
});
|
|
12411
|
+
const key = `e2e/${runId}/probe.txt`;
|
|
12412
|
+
const body = `kici aws-oidc probe run=${runId} ts=${new Date().toISOString()}\n`;
|
|
12413
|
+
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body }));
|
|
12414
|
+
const got = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
|
|
12415
|
+
const readBack = await got.Body?.transformToString();
|
|
12416
|
+
if (readBack !== body) {
|
|
12417
|
+
throw new Error(`probe object did not round-trip: got ${JSON.stringify(readBack)}`);
|
|
12418
|
+
}
|
|
12419
|
+
|
|
12420
|
+
// 4. Delete, then prove the prefix is empty.
|
|
12421
|
+
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key }));
|
|
12422
|
+
const left = await s3.send(
|
|
12423
|
+
new ListObjectsV2Command({ Bucket: BUCKET, Prefix: `e2e/${runId}/` }),
|
|
12424
|
+
);
|
|
12425
|
+
if ((left.KeyCount ?? 0) !== 0) {
|
|
12426
|
+
throw new Error(`${left.KeyCount} object(s) left under e2e/${runId}/`);
|
|
12427
|
+
}
|
|
12428
|
+
ctx.log.info(`${PREFIX} verdict=ok bucket=${BUCKET} key=${key} deleted=true`);
|
|
12429
|
+
}),
|
|
12430
|
+
],
|
|
12431
|
+
}),
|
|
12432
|
+
],
|
|
12433
|
+
});
|
|
12434
|
+
```
|
|
12435
|
+
|
|
12436
|
+
<!-- END GENERATED: oidc-aws-workflow -->
|
|
12437
|
+
|
|
12438
|
+
On the AWS side, create an IAM OpenID Connect identity provider whose URL is
|
|
12439
|
+
your orchestrator's issuer and whose audience is `sts.amazonaws.com`, then
|
|
12440
|
+
attach the [worked trust policy](https://docs.kici.dev/user/oidc/#a-worked-aws-trust-policy) to the role. When
|
|
12441
|
+
the issuer URL carries a path, the condition keys carry it too:
|
|
12442
|
+
`orch.example.com/kici:sub`, not `orch.example.com:sub`.
|
|
12443
|
+
|
|
12444
|
+
## ID-token claims and cloud trust policies
|
|
12445
|
+
|
|
12446
|
+
A cloud provider's OIDC trust policy decides which builds may assume a role. The
|
|
12447
|
+
token below is what your policy matches on, so read this section before you
|
|
12448
|
+
write one.
|
|
12449
|
+
|
|
12450
|
+
### The claim set
|
|
12451
|
+
|
|
12452
|
+
| Claim | Value |
|
|
12453
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
12454
|
+
| `iss` | Your orchestrator's provenance issuer |
|
|
12455
|
+
| `aud` | The audience you asked for |
|
|
12456
|
+
| `sub` | The build identity — see the two shapes below |
|
|
12457
|
+
| `repository` | `owner/repo` the run acted on |
|
|
12458
|
+
| `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
|
|
12459
|
+
| `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
|
|
12460
|
+
| `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
|
|
12461
|
+
| `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
|
|
12462
|
+
| `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
|
|
12463
|
+
| `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
|
|
12464
|
+
| `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
|
|
12465
|
+
| `actor` | Provider login of the triggering actor |
|
|
12466
|
+
| `sha` | The run's commit |
|
|
12467
|
+
| `workflow_ref` | `<workflow name>@<sha>` |
|
|
12468
|
+
| `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
|
|
12469
|
+
| `org_id` | Your organization id |
|
|
12470
|
+
| `orchestrator_id` | The orchestrator that ran the job |
|
|
12471
|
+
| `provider` | The source provider the run came from (`github`, `gitlab`, …) |
|
|
12472
|
+
| `source_origin` | `triggered` for a webhook-driven run; `run-remote` when the run executed an uploaded working tree (`kici run remote`) |
|
|
12473
|
+
| `attestation_origin` | `live` when the token was minted during the job; `deferred` or `offline-backfill` when it was minted later for a frozen statement |
|
|
12474
|
+
| `statement_hash` | The hash of the frozen statement a deferred token is bound to; `null` for a live token |
|
|
12475
|
+
|
|
12476
|
+
Every claim in the table is **always present**. A value the run did not resolve
|
|
12477
|
+
is `''`, `'unresolved'` or `null`, never omitted and never guessed. That matters: an
|
|
12478
|
+
absent claim makes a `StringEquals` condition pass, which would silently remove
|
|
12479
|
+
a constraint you wrote expecting it to be enforced.
|
|
12480
|
+
|
|
12481
|
+
### The two `sub` shapes
|
|
12482
|
+
|
|
12483
|
+
```
|
|
12484
|
+
push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
|
|
12485
|
+
pull request, review repo:<owner/repo>:pull_request
|
|
12486
|
+
```
|
|
12487
|
+
|
|
12488
|
+
The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
|
|
12489
|
+
pull request's `ref` is its base branch. So a ref-bearing subject would be
|
|
12490
|
+
identical for a fork pull request targeting `main` and a trusted push to `main`.
|
|
12491
|
+
A policy pinning that subject would hand your cloud role to any contributor who
|
|
12492
|
+
opened a pull request running the same workflow.
|
|
12493
|
+
|
|
12494
|
+
**A re-run keeps the shape of the run it repeats.** Re-running a pull-request
|
|
12495
|
+
run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
|
|
12496
|
+
commit from the same source. Its `event_name` claim still reads `rerun` — that
|
|
12497
|
+
claim says what started the run, while `sub` says which identity the run
|
|
12498
|
+
presents. A policy that pins the branch-shaped subject therefore does not match
|
|
12499
|
+
a re-run of a pull request, which is the same protection the first run gets.
|
|
12500
|
+
|
|
12501
|
+
### A worked AWS trust policy
|
|
12502
|
+
|
|
12503
|
+
Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
|
|
12504
|
+
ran; it does not tell you whose code ran.
|
|
12505
|
+
|
|
12506
|
+
```json
|
|
12507
|
+
{
|
|
12508
|
+
"Version": "2012-10-17",
|
|
12509
|
+
"Statement": [
|
|
12510
|
+
{
|
|
12511
|
+
"Effect": "Allow",
|
|
12512
|
+
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
|
|
12513
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
12514
|
+
"Condition": {
|
|
12515
|
+
"StringEquals": {
|
|
12516
|
+
"orch.example.com:aud": "sts.amazonaws.com",
|
|
12517
|
+
"orch.example.com:sub": "repo:acme/app:ref:main:workflow:upload",
|
|
12518
|
+
"orch.example.com:is_fork": "false",
|
|
12519
|
+
"orch.example.com:head_repository": "acme/app",
|
|
12520
|
+
"orch.example.com:trust_tier": "trusted"
|
|
12521
|
+
}
|
|
12522
|
+
}
|
|
12523
|
+
}
|
|
12524
|
+
]
|
|
12525
|
+
}
|
|
12526
|
+
```
|
|
12527
|
+
|
|
12528
|
+
This grants the role only to a run on `main` in `acme/app`, from code in that
|
|
12529
|
+
same repository, triggered by an actor your orchestrator resolved as trusted.
|
|
12530
|
+
A fork pull request fails on every one of the extra conditions, and a run whose
|
|
12531
|
+
context did not resolve fails too — `'unresolved'` matches none of them, so the
|
|
12532
|
+
policy fails closed.
|
|
12533
|
+
|
|
12534
|
+
To let a same-repo pull request assume the role, add a second statement pinning
|
|
12535
|
+
`"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
|
|
12536
|
+
`"head_repository": "acme/app"`.
|
|
12537
|
+
|
|
12538
|
+
### Migrating an existing policy
|
|
12539
|
+
|
|
12540
|
+
If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
|
|
12541
|
+
matching once you upgrade — which is the fix, because it was matching runs it
|
|
12542
|
+
should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
|
|
12543
|
+
conditions above. The same move covers a re-run of a pull request, which
|
|
12544
|
+
presents the pull-request subject too.
|
|
12545
|
+
|
|
12546
|
+
## See also
|
|
12547
|
+
|
|
12548
|
+
- [Build provenance and attestations](https://docs.kici.dev/user/provenance/) — `ctx.attestProvenance`
|
|
12549
|
+
builds on the identity token this page mints.
|
|
12550
|
+
- [Private registries](https://docs.kici.dev/user/private-registries/) — registry auth for installs,
|
|
12551
|
+
and where an exchanged Cloudsmith token fits.
|
|
12552
|
+
- [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.kici.oidc.token` step
|
|
12553
|
+
API in full.
|
|
12554
|
+
- [Signing keys](https://docs.kici.dev/operator/orchestrator/signing-keys/) — provisioning and
|
|
12555
|
+
rotating the key that signs these tokens.
|
|
12556
|
+
- [Network requirements](https://docs.kici.dev/operator/network-requirements/) — what the
|
|
12557
|
+
exchanging service must reach.
|
|
12558
|
+
|
|
12559
|
+
---
|
|
12560
|
+
|
|
12086
12561
|
## Private npm registries
|
|
12087
12562
|
|
|
12088
12563
|
Source: https://docs.kici.dev/user/private-registries/
|
|
@@ -12306,6 +12781,10 @@ registries: [
|
|
|
12306
12781
|
],
|
|
12307
12782
|
```
|
|
12308
12783
|
|
|
12784
|
+
To publish to Cloudsmith from a step without a stored token, exchange the job's
|
|
12785
|
+
OIDC identity token for a Cloudsmith credential — see
|
|
12786
|
+
[exchanging the token with an external service](https://docs.kici.dev/user/oidc/#exchanging-the-token-with-an-external-service).
|
|
12787
|
+
|
|
12309
12788
|
## Security model
|
|
12310
12789
|
|
|
12311
12790
|
- **Per-context scoping.** Every `tokenSecret` and `installEnv` entry is qualified with a context name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named context **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch. A reviewer-gated install context **pauses** the whole workflow dispatch as a workflow-scoped held run instead of resolving the token — see [Reviewer-gated installs](https://docs.kici.dev/user/private-registries/#reviewer-gated-installs) below.
|
|
@@ -12337,7 +12816,7 @@ The orchestrator exposes Prometheus counters and a histogram under the `kici_orc
|
|
|
12337
12816
|
| ------------------------------------------------------------- | --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
12338
12817
|
| `kici_orch_install_secrets_decisions_total` | Counter | `decision`, `reason` | Pass / reject / hold volume. `decision=hold` (reason `held`) counts dispatches paused at a reviewer-gated install context. Reject reasons enumerate the failure mode: `malformed_ref`, `invalid_url_scheme`, `env_not_found`, `protection_rule_block`, `missing_token`, `missing_install_env`, etc. |
|
|
12339
12818
|
| `kici_orch_install_secrets_npm_registry_used_total` | Counter | `channel`, `provider`, `scope` | Per-channel + per-scope usage. `channel=registries` is Option A, `channel=install_env` is Option C. `scope=default` marks a no-scope default registry; `scope=-` marks Option C entries. |
|
|
12340
|
-
| `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs
|
|
12819
|
+
| `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs). Expected to be 0 in single-tenant orgs. |
|
|
12341
12820
|
| `kici_orch_install_secrets_token_resolution_duration_seconds` | Histogram | `environment` | Latency of per-environment secret resolution. Pathological tails (>500ms) usually mean a Vault timeout or a slow Postgres replica. |
|
|
12342
12821
|
|
|
12343
12822
|
The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-secrets.json`; if you maintain your own monitoring stack, you can import it directly.
|
|
@@ -12434,7 +12913,10 @@ path or a precomputed digest:
|
|
|
12434
12913
|
|
|
12435
12914
|
The identity token is fetched and masked in logs automatically — you never
|
|
12436
12915
|
handle it. The call returns `{ storageKey, subjectDigest, bundleMediaType }`
|
|
12437
|
-
identifying the stored bundle.
|
|
12916
|
+
identifying the stored bundle. `ctx.attestProvenance` builds on the run's OIDC
|
|
12917
|
+
identity token; to mint that token yourself and exchange it with Cloudsmith,
|
|
12918
|
+
AWS, or any OIDC-federated service, see
|
|
12919
|
+
[Workload identity with OIDC](https://docs.kici.dev/user/oidc/).
|
|
12438
12920
|
|
|
12439
12921
|
`ctx.attestProvenance` is only available inside a running job step; calling it
|
|
12440
12922
|
outside one rejects with a clear error. `kici run --local` runs are supported:
|
|
@@ -12442,128 +12924,6 @@ the offline local dev plane signs with a dev identity under the
|
|
|
12442
12924
|
clearly-non-production issuer `kici-local`, and those bundles verify against a
|
|
12443
12925
|
trust root exported with `kici local trust-root`.
|
|
12444
12926
|
|
|
12445
|
-
### Requesting a raw identity token
|
|
12446
|
-
|
|
12447
|
-
`ctx.attestProvenance` builds on a lower-level primitive you can call directly
|
|
12448
|
-
when you need the identity token for a different tool:
|
|
12449
|
-
|
|
12450
|
-
```typescript
|
|
12451
|
-
step('mint', async (ctx) => {
|
|
12452
|
-
const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
|
|
12453
|
-
ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
|
|
12454
|
-
// Hand `token` to a tool that exchanges it with a service trusting the issuer.
|
|
12455
|
-
});
|
|
12456
|
-
```
|
|
12457
|
-
|
|
12458
|
-
The token is a short-lived (about 10 minutes) signed JWT scoped to the current
|
|
12459
|
-
run and job. Its identity claims (`repository`, `ref`, `sha`, `kici_run_id`,
|
|
12460
|
-
`kici_job_id`) are derived by the orchestrator from the run context, so a step
|
|
12461
|
-
cannot spoof them. The returned token value is automatically masked in step logs,
|
|
12462
|
-
and the step never holds signing credentials — the orchestrator mints and signs
|
|
12463
|
-
the token on the step's behalf from its own run records. Like `attestProvenance`,
|
|
12464
|
-
it is only available inside a running job step.
|
|
12465
|
-
|
|
12466
|
-
## ID-token claims and cloud trust policies
|
|
12467
|
-
|
|
12468
|
-
A cloud provider's OIDC trust policy decides which builds may assume a role. The
|
|
12469
|
-
token below is what your policy matches on, so read this section before you
|
|
12470
|
-
write one.
|
|
12471
|
-
|
|
12472
|
-
### The claim set
|
|
12473
|
-
|
|
12474
|
-
| Claim | Value |
|
|
12475
|
-
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
12476
|
-
| `iss` | Your orchestrator's provenance issuer |
|
|
12477
|
-
| `aud` | The audience you asked for |
|
|
12478
|
-
| `sub` | The build identity — see the two shapes below |
|
|
12479
|
-
| `repository` | `owner/repo` the run acted on |
|
|
12480
|
-
| `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
|
|
12481
|
-
| `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
|
|
12482
|
-
| `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
|
|
12483
|
-
| `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
|
|
12484
|
-
| `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
|
|
12485
|
-
| `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
|
|
12486
|
-
| `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
|
|
12487
|
-
| `actor` | Provider login of the triggering actor |
|
|
12488
|
-
| `sha` | The run's commit |
|
|
12489
|
-
| `workflow_ref` | `<workflow name>@<sha>` |
|
|
12490
|
-
| `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
|
|
12491
|
-
| `org_id` | Your organization id |
|
|
12492
|
-
|
|
12493
|
-
Every claim in the table is **always present**. A value the run did not resolve
|
|
12494
|
-
is `''` or `'unresolved'`, never omitted and never guessed. That matters: an
|
|
12495
|
-
absent claim makes a `StringEquals` condition pass, which would silently remove
|
|
12496
|
-
a constraint you wrote expecting it to be enforced.
|
|
12497
|
-
|
|
12498
|
-
### The two `sub` shapes
|
|
12499
|
-
|
|
12500
|
-
```
|
|
12501
|
-
push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
|
|
12502
|
-
pull request, review repo:<owner/repo>:pull_request
|
|
12503
|
-
```
|
|
12504
|
-
|
|
12505
|
-
The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
|
|
12506
|
-
pull request's `ref` is its base branch. So a ref-bearing subject would be
|
|
12507
|
-
identical for a fork pull request targeting `main` and a trusted push to `main`.
|
|
12508
|
-
A policy pinning that subject would hand your cloud role to any contributor who
|
|
12509
|
-
opened a pull request running the same workflow.
|
|
12510
|
-
|
|
12511
|
-
**A re-run keeps the shape of the run it repeats.** Re-running a pull-request
|
|
12512
|
-
run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
|
|
12513
|
-
commit from the same source. Its `event_name` claim still reads `rerun` — that
|
|
12514
|
-
claim says what started the run, while `sub` says which identity the run
|
|
12515
|
-
presents. A policy that pins the branch-shaped subject therefore does not match
|
|
12516
|
-
a re-run of a pull request, which is the same protection the first run gets.
|
|
12517
|
-
|
|
12518
|
-
### A worked AWS trust policy
|
|
12519
|
-
|
|
12520
|
-
Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
|
|
12521
|
-
ran; it does not tell you whose code ran.
|
|
12522
|
-
|
|
12523
|
-
```json
|
|
12524
|
-
{
|
|
12525
|
-
"Version": "2012-10-17",
|
|
12526
|
-
"Statement": [
|
|
12527
|
-
{
|
|
12528
|
-
"Effect": "Allow",
|
|
12529
|
-
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
|
|
12530
|
-
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
12531
|
-
"Condition": {
|
|
12532
|
-
"StringEquals": {
|
|
12533
|
-
"orch.example.com:aud": "sts.amazonaws.com",
|
|
12534
|
-
"orch.example.com:sub": "repo:acme/app:ref:main:workflow:deploy",
|
|
12535
|
-
"orch.example.com:is_fork": "false",
|
|
12536
|
-
"orch.example.com:head_repository": "acme/app",
|
|
12537
|
-
"orch.example.com:trust_tier": "trusted"
|
|
12538
|
-
}
|
|
12539
|
-
}
|
|
12540
|
-
}
|
|
12541
|
-
]
|
|
12542
|
-
}
|
|
12543
|
-
```
|
|
12544
|
-
|
|
12545
|
-
This grants the role only to a run on `main` in `acme/app`, from code in that
|
|
12546
|
-
same repository, triggered by an actor your orchestrator resolved as trusted.
|
|
12547
|
-
A fork pull request fails on all three of the extra conditions, and a run whose
|
|
12548
|
-
context did not resolve fails too — `'unresolved'` matches none of them, so the
|
|
12549
|
-
policy fails closed.
|
|
12550
|
-
|
|
12551
|
-
To let a same-repo pull request assume the role, add a second statement pinning
|
|
12552
|
-
`"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
|
|
12553
|
-
`"head_repository": "acme/app"`.
|
|
12554
|
-
|
|
12555
|
-
### Migrating an existing policy
|
|
12556
|
-
|
|
12557
|
-
If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
|
|
12558
|
-
matching once you upgrade — which is the fix, because it was matching runs it
|
|
12559
|
-
should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
|
|
12560
|
-
conditions above. The same move covers a re-run of a pull request, which
|
|
12561
|
-
presents the pull-request subject too.
|
|
12562
|
-
|
|
12563
|
-
While you migrate, `KICI_OIDC_LEGACY_PR_SUB=1` on the orchestrator restores the
|
|
12564
|
-
old subject. It restores the collision with it, so treat it as a short bridge,
|
|
12565
|
-
not a setting. See [deprecations](https://docs.kici.dev/user/deprecations/).
|
|
12566
|
-
|
|
12567
12927
|
## Verifying an attestation
|
|
12568
12928
|
|
|
12569
12929
|
Verify a bundle with the `kici verify-attestation` command. It establishes the
|
|
@@ -12739,8 +13099,10 @@ This page shows the parsed provenance for one attestation.
|
|
|
12739
13099
|
|
|
12740
13100
|
## See also
|
|
12741
13101
|
|
|
12742
|
-
- [
|
|
12743
|
-
|
|
13102
|
+
- [Workload identity with OIDC](https://docs.kici.dev/user/oidc/) — minting the identity token
|
|
13103
|
+
yourself and exchanging it with an external service.
|
|
13104
|
+
- [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.attestProvenance` step
|
|
13105
|
+
API in full.
|
|
12744
13106
|
- [CLI reference](https://docs.kici.dev/user/cli/notifications-and-diagnostics/#kici-verify-attestation) — every
|
|
12745
13107
|
`kici verify-attestation` flag and exit code.
|
|
12746
13108
|
|
|
@@ -13664,7 +14026,7 @@ The KiCI CLI reads the following environment variables to customize its behavior
|
|
|
13664
14026
|
|
|
13665
14027
|
| Variable | Description | Default |
|
|
13666
14028
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
13667
|
-
| `KICI_DEV` | Enable development mode. When `true`,
|
|
14029
|
+
| `KICI_DEV` | Enable development mode. When `true`, pins `@kici-dev/sdk` to the `latest` dist-tag (so a dev registry's prerelease build resolves) and skips npm version resolution. | unset |
|
|
13668
14030
|
| `KICI_DEV_REGISTRY` | npm registry the `@kici-dev` scope points at when `KICI_DEV` is set. `kici init` writes it into `.npmrc`; with no value it writes no `.npmrc`. | unset |
|
|
13669
14031
|
| `KICI_DEBUG` | Enable debug logging. When `true`, prints verbose diagnostics (SDK alias resolution, step-level debug logs, stack traces on errors). Equivalent to the `--debug` CLI flag. | unset |
|
|
13670
14032
|
|
|
@@ -14034,7 +14396,7 @@ The first two treat "no tier" as untrusted. The last two treat it as "no opinion
|
|
|
14034
14396
|
|
|
14035
14397
|
A subscriber that inherits a tier below `trusted` loses its install secrets. A job that installs from a private registry then fails at install time.
|
|
14036
14398
|
|
|
14037
|
-
A `minimumTrust` context holds an `unknown` subscriber for security review
|
|
14399
|
+
A `minimumTrust: 'trusted'` context holds an `unknown` subscriber for security review. Trust is a ref-based judgement with two answers, so that is the only floor a context can declare.
|
|
14038
14400
|
|
|
14039
14401
|
Both symptoms appear far from their cause. The tier belongs to the **emitting** run, so read that run's tier first.
|
|
14040
14402
|
|
|
@@ -14496,9 +14858,8 @@ Global workflows are gated by a **fleet-wide master switch** held by the orchest
|
|
|
14496
14858
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
14497
14859
|
| Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
|
|
14498
14860
|
| Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
|
|
14499
|
-
| Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
|
|
14500
14861
|
|
|
14501
|
-
|
|
14862
|
+
Both lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
|
|
14502
14863
|
|
|
14503
14864
|
Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
|
|
14504
14865
|
|
|
@@ -14533,8 +14894,6 @@ This is about your **stored secrets**, not about repository access: the job is s
|
|
|
14533
14894
|
|
|
14534
14895
|
To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
|
|
14535
14896
|
|
|
14536
|
-
The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
|
|
14537
|
-
|
|
14538
14897
|
## When does it fire?
|
|
14539
14898
|
|
|
14540
14899
|
Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire on pushes to `myorg/app`. Cross-repo globals fire on pushes to any source repo whose identifier matches a glob on the authoring workflow's trigger. The orchestrator de-duplicates between the per-repo and cross-repo matching passes, so a single event produces at most one run per (workflow, source-repo, trigger) triple.
|
|
@@ -14587,7 +14946,7 @@ A `filter` reads the source tree, so the evaluation must be able to obtain one.
|
|
|
14587
14946
|
| Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
|
|
14588
14947
|
| A global workflow is never registered at all — it is absent from `kici-admin registration list` | The fleet-wide master switch is off, or the authoring repo does not match a populated _Allowed author repos_ list. | Orchestrator log: `Global workflows excluded from registration`, naming the organization it decided against. Check the switch first (`kici-admin cluster-settings show`), then that org's allow-list in the dashboard. An `"orgId": "__default__"` in the line is not itself the fault — that anchor carries no lists and restricts nothing. |
|
|
14589
14948
|
| `repos:` has no effect — workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
|
|
14590
|
-
| Secrets unavailable in a global job | Expected — a global workflow's job receives no secrets at all
|
|
14949
|
+
| Secrets unavailable in a global job | Expected — a global workflow's job receives no secrets at all. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
|
|
14591
14950
|
| Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
|
|
14592
14951
|
| Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
|
|
14593
14952
|
| Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
|
|
@@ -14927,7 +15286,7 @@ Bind provisioning and teardown workflows to a context that carries the cloud cre
|
|
|
14927
15286
|
|
|
14928
15287
|
## The cloud-init that starts the agent
|
|
14929
15288
|
|
|
14930
|
-
`buildAgentCloudInit(creds, options)` renders the `#cloud-config` that boots the KiCI agent.
|
|
15289
|
+
`buildAgentCloudInit(creds, options)` renders the `#cloud-config` that boots the KiCI agent. It writes the single-use claim code — never a token — into a root-only env file (`0600`, owned by root). The agent exchanges that code for its own token inside the instance, so the token never transits cloud-init, the instance metadata, or any other provisioning channel. The env file holds:
|
|
14931
15290
|
|
|
14932
15291
|
- `KICI_ORCHESTRATOR_URL` — from `creds.orchestratorUrl`.
|
|
14933
15292
|
- `KICI_SCALER_CLAIM_CODE` — from `creds.claimCode`. The agent exchanges it for its own token in-instance.
|
|
@@ -14943,11 +15302,14 @@ The `0600` env file still protects the non-secret env from other users on the in
|
|
|
14943
15302
|
|
|
14944
15303
|
Pass any of these options to shape the boot:
|
|
14945
15304
|
|
|
15305
|
+
- `agentImage` — the agent image `deliveryMode: 'container'` runs. Defaults to `quay.io/kici-dev/kici-agent:latest`.
|
|
15306
|
+
- `startCommand` — an escape hatch that replaces the whole agent-start command. It ignores `deliveryMode` and `agentImage`.
|
|
14946
15307
|
- `packages` — extra apt/yum packages, merged into the cloud-init `packages:` list.
|
|
14947
15308
|
- `writeFiles` — extra `write_files` entries (path, content, permissions, owner). The reserved env-file path is rejected, so a custom file cannot overwrite the credentials.
|
|
14948
15309
|
- `runcmdBefore` / `runcmdAfter` — shell lines that run before or after the agent starts.
|
|
14949
15310
|
- `agentEnv` — extra variables appended to the agent env file. Keys must be valid env names, and a value with a newline is rejected.
|
|
14950
15311
|
- `baseCloudConfig` — a raw cloud-config document to merge everything into (users, ssh keys, apt mirrors, mounts, bootcmd). The builder unions its `packages`, `runcmd`, and `write_files` with yours.
|
|
15312
|
+
- `userDataEncoding` — `'raw'` (the default) returns the plain `#cloud-config` text that Hetzner `user_data` expects; `'base64'` returns it base64-encoded, the form AWS EC2 `UserData` and Azure `customData` expect.
|
|
14951
15313
|
|
|
14952
15314
|
## The teardown workflow
|
|
14953
15315
|
|
|
@@ -15118,7 +15480,7 @@ scalers:
|
|
|
15118
15480
|
|
|
15119
15481
|
On `kici.scaler.scale-up`, the provisioning workflow dispatches a `kici-agent.yml` workflow run in a GitHub repo. It passes the claim code, orchestrator URL, agent id, and labels as dispatch inputs. The token never appears in those inputs — only the single-use claim code, which the agent exchanges for its own token in-instance.
|
|
15120
15482
|
|
|
15121
|
-
The `kici-agent.yml` run starts the agent on the runner itself with `KICI_SCALER_CLAIM_CODE` set. `KICI_SCALER_MANAGED=1` and a zero idle timeout make the agent register, run one job, and exit. The GitHub Actions run then completes on its own. By default the run installs the published agent from npm; set `agent_bundle_release` to a release tag holding a `kici-admin agent package` tarball to pin an exact build or to serve runners that cannot reach npm.
|
|
15483
|
+
The `kici-agent.yml` run starts the agent on the runner itself with `KICI_SCALER_CLAIM_CODE` set. `KICI_SCALER_MANAGED=1` and a zero idle timeout make the agent register, run one job, and exit. The GitHub Actions run then completes on its own. The job carries `timeout-minutes: 15`. A wedged run (an agent the orchestrator refused, a job that never dispatched) then releases the runner instead of holding it for the six-hour default. Raise the limit if your jobs run longer. By default the run installs the published agent from npm; set `agent_bundle_release` to a release tag holding a `kici-admin agent package` tarball to pin an exact build or to serve runners that cannot reach npm.
|
|
15122
15484
|
|
|
15123
15485
|
Teardown is largely automatic. A GitHub Actions run self-completes when its agent exits. So the `kici.scaler.scale-down` workflow only cancels a run GitHub has not yet marked finished, and only for reasons where the agent will never do useful work (`spawn-timeout`, `heartbeat-timeout`).
|
|
15124
15486
|
|
|
@@ -15888,8 +16250,8 @@ kici-admin org-settings global-workflows deny-add \
|
|
|
15888
16250
|
```
|
|
15889
16251
|
|
|
15890
16252
|
See [Global workflows](https://docs.kici.dev/architecture/global-workflows/) for the
|
|
15891
|
-
policy model (`isWorkflowRepoAllowed` + `isSourceRepoAllowed`
|
|
15892
|
-
|
|
16253
|
+
policy model (`isWorkflowRepoAllowed` + `isSourceRepoAllowed`) and the
|
|
16254
|
+
cross-provider dispatch contract.
|
|
15893
16255
|
|
|
15894
16256
|
## Routing-key collisions
|
|
15895
16257
|
|
|
@@ -16340,7 +16702,7 @@ Orchestrator Agent Sandbox (child pro
|
|
|
16340
16702
|
| | |
|
|
16341
16703
|
|-- job.dispatch (WS) ------------>| |
|
|
16342
16704
|
| (jobConfig, sourceTarUrl, | |
|
|
16343
|
-
|
|
|
16705
|
+
| sourceTarDigest, depsUrl, |-- Create sandbox ------->|
|
|
16344
16706
|
| depsHash) | (container/bare-metal/ |
|
|
16345
16707
|
| | firecracker) |
|
|
16346
16708
|
| | |-- Restore .kici/ source (tarball)
|
|
@@ -16522,6 +16884,8 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
|
|
|
16522
16884
|
|
|
16523
16885
|
When multiple webhooks trigger simultaneously for the same repository state, the `BuildCoordinator` coalesces concurrent build requests using a combined key (`contentHash:lockfileHash`). Only one build job runs; all waiting dispatches share the result.
|
|
16524
16886
|
|
|
16887
|
+
The coalescing map is per coordinator. The build job itself sits on the cluster-wide dispatch queue, so an agent connected to a sibling coordinator can claim it. That sibling persists the terminal `job.status` to the shared `execution_jobs` row (`precursor_result`), and the dispatching coordinator's `PendingPrecursorDbWatcher` reads the row back and settles the pending build through `settlePendingPrecursor()`. The local agent socket goes through the same function. See [runs that span coordinators](https://docs.kici.dev/operator/orchestrator/clustering/#runs-that-span-coordinators).
|
|
16888
|
+
|
|
16525
16889
|
### Graceful degradation
|
|
16526
16890
|
|
|
16527
16891
|
If cache storage is unavailable or a download fails:
|
|
@@ -16596,7 +16960,7 @@ Agent Orchestrator S3
|
|
|
16596
16960
|
| | for deps integrity) |
|
|
16597
16961
|
```
|
|
16598
16962
|
|
|
16599
|
-
The two-phase metadata approach (`upload via PUT` then `initMeta via CopyObject`) works around the limitation that S3 pre-signed URLs cannot include custom metadata headers. For dependency tarballs, the agent also reports the SHA-256 content hash in `cache.upload.complete`; the orchestrator stores it as a companion `.hash` file alongside the tarball. When dispatching execution jobs, the orchestrator reads this hash and includes it as `depsHash` in `job.dispatch`, enabling agent-side integrity verification on download. Source tarballs
|
|
16963
|
+
The two-phase metadata approach (`upload via PUT` then `initMeta via CopyObject`) works around the limitation that S3 pre-signed URLs cannot include custom metadata headers. For dependency tarballs, the agent also reports the SHA-256 content hash in `cache.upload.complete`; the orchestrator stores it as a companion `.hash` file alongside the tarball. When dispatching execution jobs, the orchestrator reads this hash and includes it as `depsHash` in `job.dispatch`, enabling agent-side integrity verification on download. Source tarballs carry their own SHA-256 as `sourceTarDigest` in `job.dispatch`, verified before extraction; the workflow `contentHash` is then re-computed against the extracted source to verify it against the lock file, which covers drift end-to-end.
|
|
16600
16964
|
|
|
16601
16965
|
### URL delivery (downloads)
|
|
16602
16966
|
|
|
@@ -17095,7 +17459,7 @@ The compiler processes the workflow definition:
|
|
|
17095
17459
|
|
|
17096
17460
|
When `kici run <event> --local` runs a workflow:
|
|
17097
17461
|
|
|
17098
|
-
1. **SDK module resolution:** The runner resolves `setStepOutputsMap` / `setJobOutputsMap` from the
|
|
17462
|
+
1. **SDK module resolution:** The runner resolves `setStepOutputsMap` / `setJobOutputsMap` from the `@kici-dev/sdk/internal` subpath of the same SDK copy that the workflow uses (ensures the proxy reads from the same map)
|
|
17099
17463
|
2. **Map injection:** Fresh `OutputsMap` and `StepRefMap` are created and injected via `setStepOutputsMap()` / `setStepRefMap()` before each job
|
|
17100
17464
|
3. **Step execution:** Each step runs sequentially. If the step returns a value, it is stored in the `OutputsMap` keyed by step name
|
|
17101
17465
|
4. **Bare function normalization:** Bare functions in the steps array are assigned counter names and registered in the `StepRefMap` (maps function reference to step name)
|
|
@@ -17230,7 +17594,7 @@ The Platform never processes, stores, or executes customer code, and never sees
|
|
|
17230
17594
|
|
|
17231
17595
|
The orchestrator is the execution brain. It decides what to run and dispatches work to agents.
|
|
17232
17596
|
|
|
17233
|
-
- **Trigger matching** -- Evaluates lock file triggers against webhook payloads to determine which jobs to run. Uses branch, path, and event matching
|
|
17597
|
+
- **Trigger matching** -- Evaluates lock file triggers against webhook payloads to determine which jobs to run. Uses glob-based branch, path, and event matching.
|
|
17234
17598
|
- **Lock file caching** -- Fetches `kici.lock.json` via the configured source's fetcher (GitHub API, universal-git clone for generic webhook sources backed by a git URL, or the local filesystem for `file://` sources). An LRU cache wraps the per-provider fetcher, keyed by `{provider}:{repo}:{ref}` so cross-provider fallback resolutions stay isolated.
|
|
17235
17599
|
- **Agent registry** -- Tracks connected agents with label-based routing for job dispatch.
|
|
17236
17600
|
- **Job queue** -- PostgreSQL-backed FIFO queue for reliable dispatch.
|
|
@@ -17262,7 +17626,7 @@ The agent is the execution worker. It runs on customer infrastructure and has fu
|
|
|
17262
17626
|
Shared business logic used by all three tiers. Single source of truth for cross-tier concerns. Has no internal `@kici-dev/*` dependencies -- only a handful of third-party libraries.
|
|
17263
17627
|
|
|
17264
17628
|
- Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, the test-relay control plane, log pull, run events, peer-to-peer, cluster join, and source registration)
|
|
17265
|
-
- Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, FileContentsFetcher, CloneTokenProvider, RepoUrlBuilder, CheckStatusPoster)
|
|
17629
|
+
- Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, FileContentsFetcher, CloneTokenProvider, RepoUrlBuilder, CheckStatusPoster)
|
|
17266
17630
|
- Git credential vocabulary (forge names plus the credential reference, grant, request, and result shapes the SDK declares and the orchestrator's broker resolves) and the agent→orchestrator relay protocol its credential helper calls. See [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/)
|
|
17267
17631
|
- Trigger matching engine (branch, path, event evaluation)
|
|
17268
17632
|
- Content-requirement matcher (the declarative `requires` filter -- pure data describing a query over the bytes of one source file at the event's ref, interpreted by the orchestrator via the `FileContentsFetcher` so no author code runs there) and the shared text-match vocabulary (`contains` / `notContains` / `matches` / `notMatches`) it shares with the commit-message trigger filter
|
|
@@ -17312,7 +17676,7 @@ It also runs the **local dev plane** -- an on-demand, fully local execution stac
|
|
|
17312
17676
|
|
|
17313
17677
|
### `@kici-dev/core`
|
|
17314
17678
|
|
|
17315
|
-
Light shared utilities with no server-side dependencies. It provides JSON-structured logging, error helpers, async-local-storage request context, and human-readable formatting (`formatBytes`/`formatDuration`/`formatUptime`). It also provides cryptographic helpers (`sha256`/`sha256File`/`deriveSharedSecret` plus symmetric encrypt/decrypt), retry-backoff computation, and the shared diagnostics-result contract. The rest of its surface ships as subpath entry points: the temp-directory allocator and its garbage collector, package-manager detection, CI-environment detection, and the idempotent-step runner (the check / confirm / apply primitive behind idempotent steps). One further subpath holds the `.kici/` source digest: the single content-hash definition the compiler writes into the lock file and the agent recomputes as its drift gate. Finally it supplies zx initialization (`initZx()`) and the TypeScript loader hook that transforms TypeScript on import. It is the dependency-light core that the SDK, compiler, and `kici` CLI consume directly so they stay free of heavier server-only dependencies. `@kici-dev/shared` re-exports it, so existing `@kici-dev/shared` import paths keep working.
|
|
17679
|
+
Light shared utilities with no server-side dependencies. It provides JSON-structured logging, error helpers, async-local-storage request context, and human-readable formatting (`formatBytes`/`formatDuration`/`formatUptime`). It also provides cryptographic helpers (`sha256`/`sha256File`/`deriveSharedSecret` plus symmetric encrypt/decrypt), retry-backoff computation, and the shared diagnostics-result contract. The rest of its surface ships as subpath entry points: the temp-directory allocator and its garbage collector, package-manager detection, CI-environment detection, and the idempotent-step runner (the check / confirm / apply primitive behind idempotent steps). One further subpath holds the `.kici/` source digest: the single content-hash definition the compiler writes into the lock file and the agent recomputes as its drift gate. It also owns the published docs host: `docsUrl()` builds every docs link a CLI prints or a template scaffolds from one `DOCS_SITE_URL` constant, so no other package spells the host. Finally it supplies zx initialization (`initZx()`) and the TypeScript loader hook that transforms TypeScript on import. It is the dependency-light core that the SDK, compiler, and `kici` CLI consume directly so they stay free of heavier server-only dependencies. `@kici-dev/shared` re-exports it, so existing `@kici-dev/shared` import paths keep working.
|
|
17316
17680
|
|
|
17317
17681
|
> Source: `packages/core/src/`
|
|
17318
17682
|
|
|
@@ -17394,7 +17758,7 @@ KiCI uses three WebSocket layers for real-time communication.
|
|
|
17394
17758
|
|
|
17395
17759
|
### Platform ↔ Orchestrator
|
|
17396
17760
|
|
|
17397
|
-
The orchestrator connects outbound to the Platform WebSocket endpoint. After authentication (API key validated via SHA-256 hash lookup), the connection is used for webhook relay, execution telemetry (events, status, logs), source registration, and peer discovery.
|
|
17761
|
+
The orchestrator connects outbound to the Platform WebSocket endpoint. After authentication (API key validated via SHA-256 hash lookup), the connection is used for webhook relay, execution telemetry (events, status, logs), source registration, and peer discovery. Peer discovery is matchmaking only: the Platform pushes a `peer.update` membership list to every orchestrator sharing a routing key, and the orchestrators then connect to each other directly. Inter-orchestrator traffic such as `job.reroute` never transits the Platform.
|
|
17398
17762
|
|
|
17399
17763
|
### Orchestrator ↔ Orchestrator (P2P)
|
|
17400
17764
|
|