@kici-dev/compiler 0.6.1 → 0.7.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 +1 -1
- package/dist/commands/compile.js +5 -1
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +77 -12
- package/dist/commands/preview.js +1 -1
- package/dist/commands/report/identity.d.ts +11 -0
- package/dist/commands/report/identity.js +7 -2
- package/dist/commands/run-routed.js +1 -0
- package/dist/commands/types.d.ts +6 -1
- package/dist/commands/types.js +2 -1
- package/dist/execution/executor.js +7 -1
- package/dist/llm-context/llms-architecture.txt +72 -86
- package/dist/llm-context/llms-cli-remote.txt +40 -7
- package/dist/llm-context/llms-cli.txt +67 -34
- package/dist/llm-context/llms-features-execution.txt +52 -6
- package/dist/llm-context/llms-features.txt +137 -6
- package/dist/llm-context/llms-full.txt +527 -174
- package/dist/llm-context/llms-getting-started.txt +3 -3
- package/dist/llm-context/llms-patterns.txt +81 -5
- package/dist/llm-context/llms-providers.txt +6 -2
- package/dist/llm-context/llms-sdk-runtime.txt +22 -18
- package/dist/llm-context/llms-sdk.txt +47 -7
- package/dist/llm-context/llms.txt +8 -8
- package/dist/local-plane/orchestrator-process.d.ts +0 -8
- package/dist/local-plane/orchestrator-process.js +3 -14
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/lockfile/generator.js +25 -9
- package/dist/lockfile/hasher.d.ts +5 -13
- package/dist/lockfile/hasher.js +1 -15
- package/dist/lockfile/workspace-siblings.d.ts +46 -0
- package/dist/lockfile/workspace-siblings.js +197 -0
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/job-executor.js +1 -1
- package/dist/test-runner/rule-evaluator.js +1 -1
- package/dist/types.d.ts +6 -1
- package/package.json +7 -9
- package/sbom.spdx.json +123 -123
- package/dist/postinstall.d.ts +0 -9
- package/dist/postinstall.js +0 -62
- package/hack/postinstall.mjs +0 -105
|
@@ -48,7 +48,7 @@ The lock file is the seam. Everything left of it is decided once at compile time
|
|
|
48
48
|
|
|
49
49
|
`kici compile` loads your `.kici/workflows/*.ts`, validates dependencies (no cycles, no missing `needs`), assigns compile-time step IDs (unnamed steps become `step-1`, `step-2`, …), and writes `kici.lock.json`.
|
|
50
50
|
|
|
51
|
-
The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure lands in the lock. Anything your top-level code computes that isn't part of the returned workflow object
|
|
51
|
+
The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure lands in the lock. Anything your top-level code computes that isn't part of the returned workflow object doesn't exist past this point.
|
|
52
52
|
|
|
53
53
|
See [compile the workflow](https://docs.kici.dev/user/getting-started/#compile-the-workflow) for the command in context.
|
|
54
54
|
|
|
@@ -204,7 +204,7 @@ kici runs logs <run-id>
|
|
|
204
204
|
When you cannot resolve it yourself, gather a diagnostic bundle:
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
kici report --run <run-id>
|
|
207
|
+
kici report --run <run-id>
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
The command writes a ZIP and prints its path and `sha256`. It sends nothing.
|
|
@@ -311,7 +311,7 @@ npx kici init
|
|
|
311
311
|
This will:
|
|
312
312
|
|
|
313
313
|
1. Create `.kici/` directory with `workflows/`, `tests/`, `types/`, `package.json`, and `tsconfig.json`. The `types/` folder holds a local development aid — TypeScript declarations that `kici types` (and an authenticated `kici compile`) generate from your orchestrator's secret contexts. Its content is a snapshot of one org's secret keys, so it is not committed.
|
|
314
|
-
2. Create
|
|
314
|
+
2. Create two `.kiciignore` files with sensible defaults: one at the repo root, which selects the working-tree files a remote run uploads, and one inside `.kici/`, which declares the paths the per-workflow content hash skips
|
|
315
315
|
3. Let you choose from starter workflow templates (hello-world, pr-checks)
|
|
316
316
|
4. Install dependencies using the package manager detected for your repo (npm, pnpm, or yarn)
|
|
317
317
|
5. Update `.gitignore` to exclude `.kici/node_modules/`, and write `.kici/.gitignore` to keep the generated `types/` declarations untracked (`kici.lock.json` stays tracked — the orchestrator fetches it from your repo)
|
|
@@ -1781,6 +1781,74 @@ For a **minted app token**, prefer re-deriving over transporting — those expir
|
|
|
1781
1781
|
after an hour, so one minted in an earlier job is often already dead by the time
|
|
1782
1782
|
a later job reads it. Have the later job name the same secret, or mint its own.
|
|
1783
1783
|
|
|
1784
|
+
## What a job may ask for
|
|
1785
|
+
|
|
1786
|
+
A credential is authorized against the workflow you wrote, not against the code
|
|
1787
|
+
running in the job. Three things must all hold before the orchestrator resolves
|
|
1788
|
+
one:
|
|
1789
|
+
|
|
1790
|
+
1. **The job declared it.** The orchestrator records the job's `gitCredentials`
|
|
1791
|
+
map when it dispatches the job, and compares every request against that
|
|
1792
|
+
record. A request naming a credential the job did not declare is refused.
|
|
1793
|
+
This is why you pass `credential: 'forge'` — a name — rather than building a
|
|
1794
|
+
credential reference in step code.
|
|
1795
|
+
2. **The named context admits the run.** A `prod:` reference runs the `prod`
|
|
1796
|
+
context's own protection rules first: its branch restrictions, its
|
|
1797
|
+
`minimumTrust`, its approval requirement. A credential named from a branch
|
|
1798
|
+
the context does not allow is refused, and the git operation fails. The rule
|
|
1799
|
+
that refused it is named in your orchestrator's log, not in the run — the
|
|
1800
|
+
orchestrator returns a fixed error to the job rather than describing its own
|
|
1801
|
+
configuration to code it does not trust.
|
|
1802
|
+
3. **The contributor is trusted.** A run from an untrusted ref — a fork pull
|
|
1803
|
+
request — gets no declared credential at all. It still clones with the
|
|
1804
|
+
source credential, so the build runs; only the declared credentials are
|
|
1805
|
+
withheld. The reduced-privilege note on the run says so.
|
|
1806
|
+
|
|
1807
|
+
The context in a reference does **not** have to appear in the job's `contexts:`
|
|
1808
|
+
list. The reference names its own context, and that context's rules are what
|
|
1809
|
+
authorize it.
|
|
1810
|
+
|
|
1811
|
+
## Generated jobs
|
|
1812
|
+
|
|
1813
|
+
A job produced by a `dynamicJob` generator has no entry in the lock file, so it
|
|
1814
|
+
cannot declare credentials of its own. The **generator** declares them, and every
|
|
1815
|
+
job it produces inherits that map:
|
|
1816
|
+
|
|
1817
|
+
```typescript
|
|
1818
|
+
dynamicJob('shards', {
|
|
1819
|
+
gitCredentials: {
|
|
1820
|
+
forge: { kind: 'token', tokenSecret: 'ci:FORGE_PAT' },
|
|
1821
|
+
},
|
|
1822
|
+
generate: async ({ ctx }) =>
|
|
1823
|
+
ctx.event.payload.targets.map((target) =>
|
|
1824
|
+
job(`publish-${target}`, {
|
|
1825
|
+
runsOn: 'linux',
|
|
1826
|
+
run: async ({ $, repo }) => {
|
|
1827
|
+
await repo.withWrite(
|
|
1828
|
+
{ permissions: { contents: 'write' }, credential: 'forge' },
|
|
1829
|
+
async () => {
|
|
1830
|
+
await $`git push origin HEAD`;
|
|
1831
|
+
},
|
|
1832
|
+
);
|
|
1833
|
+
},
|
|
1834
|
+
}),
|
|
1835
|
+
),
|
|
1836
|
+
});
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Three points follow from where the declaration lives:
|
|
1840
|
+
|
|
1841
|
+
- **All generated jobs share one map.** The generator is granted one ceiling, and
|
|
1842
|
+
every job it produces gets exactly that ceiling. Use a second generator when
|
|
1843
|
+
two sets of jobs need different credentials.
|
|
1844
|
+
- **A `gitCredentials` map on a generated job is ignored.** The generator's
|
|
1845
|
+
declaration is committed source that KiCI reads from the lock file. A generated
|
|
1846
|
+
job's own declaration would come from the code that produced it, which is what
|
|
1847
|
+
the authorization check above exists to be independent of.
|
|
1848
|
+
- **The options form is required.** `dynamicJob('shards', async () => …)` — the
|
|
1849
|
+
bare function form — has nowhere to put the declaration. Pass
|
|
1850
|
+
`{ generate, gitCredentials }` instead; `needs` stays optional.
|
|
1851
|
+
|
|
1784
1852
|
## How it works, and why long jobs still push
|
|
1785
1853
|
|
|
1786
1854
|
An app token expires an hour after it is issued, and cannot be renewed. Rather
|
|
@@ -1797,6 +1865,10 @@ does at the start, and no credential is ever written into `.git/config`, into
|
|
|
1797
1865
|
unaffected.
|
|
1798
1866
|
- **The reserved `needs:` context is not resolvable yet** on a deployed
|
|
1799
1867
|
orchestrator; naming it produces a clear error rather than a wrong credential.
|
|
1868
|
+
- **A credential reference built in step code is refused.** The SDK takes a
|
|
1869
|
+
credential _name_; there is no way to pass a reference. Code that constructs
|
|
1870
|
+
one and sends it directly is rejected by the agent and, if it reaches the
|
|
1871
|
+
orchestrator, by the declaration check above.
|
|
1800
1872
|
- **A write window is bounded by the repository and the callback, not the step.**
|
|
1801
1873
|
Steps running concurrently in the same job can push to the same repository
|
|
1802
1874
|
while it is open. They cannot reach a different one.
|
|
@@ -2131,8 +2203,10 @@ KiCI has no native provider for Gogs, Forgejo, or Gitea, but these forges send H
|
|
|
2131
2203
|
```bash
|
|
2132
2204
|
# Forgejo / Gitea send event name in X-Gitea-Event and signature in X-Gitea-Signature.
|
|
2133
2205
|
# Gogs uses X-Gogs-Event and X-Gogs-Signature (same HMAC-SHA256 hex-digest format).
|
|
2206
|
+
# --org must be your Platform organization id: a generic source's routing key
|
|
2207
|
+
# embeds it, and the Platform refuses to register a key naming another org.
|
|
2134
2208
|
kici-admin source add generic \
|
|
2135
|
-
--org
|
|
2209
|
+
--org <platform-org-id> \
|
|
2136
2210
|
--name forgejo-main \
|
|
2137
2211
|
--verification hmac_sha256 \
|
|
2138
2212
|
--secret @/path/to/webhook-secret.txt \
|
|
@@ -2140,7 +2214,7 @@ kici-admin source add generic \
|
|
|
2140
2214
|
--rate-limit 120
|
|
2141
2215
|
```
|
|
2142
2216
|
|
|
2143
|
-
Note the returned source ID, then register a webhook in the forge pointing at `https://<platform>/
|
|
2217
|
+
Note the returned source ID, then register a webhook in the forge pointing at `https://<platform>/webhook/<orgId>/generic/<sourceId>` (or the orchestrator's direct URL, which takes the source **name** in place of the id). Set content type to `application/json` and paste the same secret.
|
|
2144
2218
|
|
|
2145
2219
|
**Workflow:**
|
|
2146
2220
|
|
|
@@ -2207,14 +2281,16 @@ HTTPS with a forge PAT works the same way — store the token as a secret, `awai
|
|
|
2207
2281
|
|
|
2208
2282
|
## Plain GitHub repo webhooks (no GitHub App)
|
|
2209
2283
|
|
|
2210
|
-
The Gogs/Forgejo/Gitea pattern above also applies when you want to trigger workflows from a GitHub repository **without installing the KiCI GitHub App
|
|
2284
|
+
The Gogs/Forgejo/Gitea pattern above also applies when you want to trigger workflows from a GitHub repository **without installing the KiCI GitHub App**. You may lack org-admin rights, sit on a restricted GitHub Enterprise tenant, or not want an App installation. Model the repo-level webhook as a generic source, accepting the same `genericWebhook()`-only ergonomics.
|
|
2211
2285
|
|
|
2212
2286
|
**Operator setup:**
|
|
2213
2287
|
|
|
2214
2288
|
```bash
|
|
2215
2289
|
# GitHub sends event name in X-GitHub-Event and HMAC-SHA256 signature in X-Hub-Signature-256.
|
|
2290
|
+
# --org must be your Platform organization id: a generic source's routing key
|
|
2291
|
+
# embeds it, and the Platform refuses to register a key naming another org.
|
|
2216
2292
|
kici-admin source add generic \
|
|
2217
|
-
--org
|
|
2293
|
+
--org <platform-org-id> \
|
|
2218
2294
|
--name gh-repo-foo \
|
|
2219
2295
|
--verification hmac_sha256 \
|
|
2220
2296
|
--secret @/path/to/webhook-secret.txt \
|
|
@@ -2231,7 +2307,7 @@ curl -X PATCH https://<orchestrator>/api/v1/admin/generic-sources/<sourceId> \
|
|
|
2231
2307
|
|
|
2232
2308
|
Then in the GitHub repo, go to **Settings → Webhooks → Add webhook**, set:
|
|
2233
2309
|
|
|
2234
|
-
- **Payload URL:** `https://<platform>/
|
|
2310
|
+
- **Payload URL:** `https://<platform>/webhook/<orgId>/generic/<sourceId>` (or the orchestrator's direct URL, which takes the source **name** in place of the id)
|
|
2235
2311
|
- **Content type:** `application/json`
|
|
2236
2312
|
- **Secret:** the same secret
|
|
2237
2313
|
- **Events:** pick what you care about (e.g., `push`, `pull_request`)
|
|
@@ -2688,6 +2764,7 @@ runsOn: { labels: ['kici:os:linux'], exclude: ['kici:host:box-01'] }
|
|
|
2688
2764
|
|
|
2689
2765
|
- **Required labels:** The agent must have every label in the `labels` array (or the string/array form).
|
|
2690
2766
|
- **Excluded labels:** The agent must NOT have any label in the `exclude` array. This includes auto-derived labels like `kici:arch:arm64`, `kici:os:linux`, etc.
|
|
2767
|
+
- **Case:** Label matching is **case-insensitive** at every step. `runsOn: 'gpu'` matches an agent that reports `GPU`, and a pool declaring `["Docker"]` serves a `runsOn: ["docker"]` job (see [auto-scaler matching rules](https://docs.kici.dev/operator/orchestrator/auto-scaler/operations/#matching-rules)). KiCI stores and displays every label in lowercase, so the dashboard, `kici-admin agent list`, and `ctx.kici.inventory[…].labels` report the folded form. Compare against a lowercase value when you read a label back in workflow code: `h.labels.includes('gpu')`, not `h.labels.includes('GPU')`.
|
|
2691
2768
|
- **Compile-time validation:** The compiler will error if any label appears in both `labels` and `exclude` (overlap detection).
|
|
2692
2769
|
- **Operator-declared mandatory labels:** Operators may mark a scaler with `mandatoryLabels` (Kubernetes-taint-style opt-in). When a scaler declares a mandatory label, a job is only allowed to land on it if `runsOn.labels` includes that label. A workflow targeting such a scaler must explicitly list the mandatory label in `runsOn`. See the [auto-scaler mandatory labels](https://docs.kici.dev/operator/orchestrator/auto-scaler/common-config/#mandatory--exclude-labels) for details.
|
|
2693
2770
|
|
|
@@ -2740,6 +2817,12 @@ Every selector element — in `runsOn`, in `runsOnAll`, on both the include and
|
|
|
2740
2817
|
- **String with glob metacharacters (`*`, `?`, `[]`, `{}`) → glob.** `'kici:host:web-*'` matches every host label starting with `kici:host:web-`. `'kici:host:box-0[1-3]'` matches `box-01`, `box-02`, `box-03`.
|
|
2741
2818
|
- **`RegExp` literal → regular expression.** `/kici:host:box-0[1-3]/` matches any label the expression matches.
|
|
2742
2819
|
|
|
2820
|
+
All three forms match case-insensitively. `'GPU'`, `'kici:host:Web-*'` and `/kici:host:BOX-0[1-3]/` each match a label of any case, and a `RegExp` you write with the `i` flag behaves the same. The `g` and `y` flags are dropped — a selector asks one question per label, so a sticky match would resume part-way through the next one.
|
|
2821
|
+
|
|
2822
|
+
`kici:host:` carries the machine's hostname folded to lowercase. A host that calls itself `Build-Box-01` advertises `kici:host:build-box-01`, and both `runsOn: 'kici:host:build-box-01'` and `runsOn: 'kici:host:Build-Box-01'` match it.
|
|
2823
|
+
|
|
2824
|
+
Case folding covers labels and hostnames only. An **agent ID** stays an opaque identifier and compares exactly, which is what keeps a per-host secret binding on `prod-01` away from an agent named `PROD-01` — see [per-host secret scoping](https://docs.kici.dev/operator/security/secrets/#per-host-secret-scoping).
|
|
2825
|
+
|
|
2743
2826
|
Both the required (include) side and the excluded side accept all three forms:
|
|
2744
2827
|
|
|
2745
2828
|
```typescript
|
|
@@ -3398,11 +3481,19 @@ Tag a dynamic job generator with a group name so other jobs can reference it via
|
|
|
3398
3481
|
```typescript
|
|
3399
3482
|
function dynamicJob(
|
|
3400
3483
|
groupName: string,
|
|
3401
|
-
fnOrConfig:
|
|
3484
|
+
fnOrConfig:
|
|
3485
|
+
| DynamicJobFn
|
|
3486
|
+
| {
|
|
3487
|
+
needs?: DynamicJobNeed[];
|
|
3488
|
+
generate: DynamicJobFn;
|
|
3489
|
+
gitCredentials?: GitCredentialMap;
|
|
3490
|
+
},
|
|
3402
3491
|
): TaggedDynamicJobFn;
|
|
3403
3492
|
```
|
|
3404
3493
|
|
|
3405
|
-
The second argument is either a plain generator (event-only, evaluated at webhook time) or
|
|
3494
|
+
The second argument is either a plain generator (event-only, evaluated at webhook time) or an options config. An options config that declares `needs` is result-aware: it defers the generator until those upstreams complete and exposes their frozen outputs as `ctx.needs`. See [Rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/#dynamicjob--result-aware-generation) for the result-aware form.
|
|
3495
|
+
|
|
3496
|
+
`needs` is optional. An options config without it is evaluated at webhook time, like the plain generator form. Use that form to declare `gitCredentials` on an event-only generator: every job the generator produces inherits the map, which is the only way a generated job gets named credentials. See [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/#generated-jobs).
|
|
3406
3497
|
|
|
3407
3498
|
The generator runs twice: once in the init phase (to register expected job names) and once inside the executing agent (to produce the actual jobs). Mismatches between the two evaluations are detected as determinism drift -- see [dynamic-jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/).
|
|
3408
3499
|
|
|
@@ -3581,6 +3672,11 @@ The throw is a `ChangedFilesUnavailableError` (exported from `@kici-dev/sdk`, ca
|
|
|
3581
3672
|
|
|
3582
3673
|
### evaluateRules(rules, context, label, onRuleResult?)
|
|
3583
3674
|
|
|
3675
|
+
The agent calls this on your behalf. A workflow does not call it. It lives on
|
|
3676
|
+
`@kici-dev/sdk/internal`, outside semver, and stays exported from the root barrel as
|
|
3677
|
+
`@deprecated` until v1.0.0 — see [deprecations](https://docs.kici.dev/user/deprecations/). It is described here
|
|
3678
|
+
because its return shape is what a rule's outcome looks like in the run log.
|
|
3679
|
+
|
|
3584
3680
|
Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
|
|
3585
3681
|
|
|
3586
3682
|
```typescript
|
|
@@ -3648,7 +3744,7 @@ Every variant carries the shared `EventBase` fields — `type`, `action`, `targe
|
|
|
3648
3744
|
|
|
3649
3745
|
Matrix configurations expand a single job into multiple instances, one per parameter combination. Maximum 256 combinations.
|
|
3650
3746
|
|
|
3651
|
-
Combinations must be **unique**. Two combinations that would produce the same instance name —
|
|
3747
|
+
Combinations must be **unique**. Two combinations that would produce the same instance name — in the simplest case, the same value listed twice — fail the job instead of quietly running it twice.
|
|
3652
3748
|
|
|
3653
3749
|
Expansion happens at **dispatch time**: the orchestrator materializes the matrix into N execution jobs — one per combination, each dispatched to its own agent — before any job runs. Each instance receives its combination as `ctx.matrix`. This is identical whether the workflow runs via `kici run <event> --local` or remotely through a webhook trigger, and the dashboard groups the N instances under one parent node.
|
|
3654
3750
|
|
|
@@ -3831,8 +3927,13 @@ isDynamicFunction(matrix); // true if async function
|
|
|
3831
3927
|
|
|
3832
3928
|
### Matrix expansion utilities
|
|
3833
3929
|
|
|
3930
|
+
The agent expands a matrix for you. A workflow does not call these. They live on
|
|
3931
|
+
`@kici-dev/sdk/internal`, outside semver, and stay exported from the root barrel as
|
|
3932
|
+
`@deprecated` until v1.0.0 — see [deprecations](https://docs.kici.dev/user/deprecations/). They are described
|
|
3933
|
+
here because they define the combinations a matrix job actually produces.
|
|
3934
|
+
|
|
3834
3935
|
```typescript
|
|
3835
|
-
import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk';
|
|
3936
|
+
import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk/internal';
|
|
3836
3937
|
```
|
|
3837
3938
|
|
|
3838
3939
|
`expandMatrix(matrix)` takes a string array or an object of string arrays and returns all combinations as `MatrixValues[]`. For a single-dimension array, each value becomes `{ value: '...' }`. For multi-dimensional objects, it produces the Cartesian product. Anything else throws a `MatrixShapeError` naming the expected shape; numbers and booleans inside the values are accepted and converted to strings.
|
|
@@ -3890,7 +3991,8 @@ export default workflow('ci', {
|
|
|
3890
3991
|
`dynamicJob(group, fnOrConfig)` tags a generator with a group name (so static jobs can depend on it via `needs: [dynamicGroup('group')]`). It is polymorphic:
|
|
3891
3992
|
|
|
3892
3993
|
- **Function form** — event-only, dispatched at webhook time: `dynamicJob('shards', async ({ ctx }) => [...])`.
|
|
3893
|
-
- **Options-object form** — result-aware
|
|
3994
|
+
- **Options-object form** — `dynamicJob('reports', { needs, generate })`. With `needs`, it is result-aware: deferred until those upstreams complete, then run with their frozen outputs as `ctx.needs`.
|
|
3995
|
+
- `needs` is optional. Without it the generator is dispatched at webhook time, like the function form. That form is how a generator declares `gitCredentials`, which every job it produces inherits — see [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/#generated-jobs).
|
|
3894
3996
|
|
|
3895
3997
|
```typescript
|
|
3896
3998
|
import { workflow, job, step, dynamicJob, dynamicGroup, z } from '@kici-dev/sdk';
|
|
@@ -4063,7 +4165,7 @@ those files contain**. It is a declarative filter on the `pr()`, `push()`, and
|
|
|
4063
4165
|
at the event's commit. The orchestrator evaluates it as pure data before
|
|
4064
4166
|
dispatching — it reads only the referenced files, never clones the whole
|
|
4065
4167
|
repository, and never runs any of your workflow code. A workflow whose `requires`
|
|
4066
|
-
does not pass is
|
|
4168
|
+
does not pass is not dispatched.
|
|
4067
4169
|
|
|
4068
4170
|
Each entry is a `ContentRequirement`:
|
|
4069
4171
|
|
|
@@ -4328,7 +4430,7 @@ dispatch({ types: ['deploy', 'rollback'] }); // Specific event types
|
|
|
4328
4430
|
#### Typed dispatch inputs
|
|
4329
4431
|
|
|
4330
4432
|
A `dispatch()` trigger can declare a typed `inputs` schema. Operators supply
|
|
4331
|
-
values with `kici run --input key=value`; KiCI validates, coerces, defaults, and
|
|
4433
|
+
values with `kici run remote --input key=value`; KiCI validates, coerces, defaults, and
|
|
4332
4434
|
exposes them to steps and rules as `ctx.dispatchInputs`. The values are validated
|
|
4333
4435
|
on the orchestrator from the compiled lock file — a missing required input or a
|
|
4334
4436
|
bad value is rejected before any agent runs, without cloning the repository.
|
|
@@ -5043,6 +5145,20 @@ import { workflow, job, step, pr, push, rule, defineEvent } from '@kici-dev/sdk'
|
|
|
5043
5145
|
|
|
5044
5146
|
For the complete list of every named export (factory functions, triggers, rules, validation, hook factories, types), see the per-topic pages above.
|
|
5045
5147
|
|
|
5148
|
+
## `@kici-dev/sdk/internal` is not a supported surface
|
|
5149
|
+
|
|
5150
|
+
The package also publishes an `@kici-dev/sdk/internal` subpath. It carries the runtime
|
|
5151
|
+
contract between the SDK and the KiCI agent. Those are the functions that install the maps
|
|
5152
|
+
a `.result` proxy reads, build the step context your workflow body receives, evaluate its
|
|
5153
|
+
rules, and expand its matrix. The agent drives all of it on your behalf.
|
|
5154
|
+
|
|
5155
|
+
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/).
|
|
5161
|
+
|
|
5046
5162
|
## See also
|
|
5047
5163
|
|
|
5048
5164
|
- [Getting started](https://docs.kici.dev/user/getting-started/) -- install the SDK, write your first workflow, test locally
|
|
@@ -5671,7 +5787,7 @@ The SDK exposes three idempotency helpers — a generic function `idempotent()`,
|
|
|
5671
5787
|
2. **Apply** the change only when drift is detected.
|
|
5672
5788
|
3. **Surface** the resource (or its identifier) on both branches, so downstream steps don't need to know whether work happened or was skipped.
|
|
5673
5789
|
|
|
5674
|
-
`idempotent()` and `idempotentStep()` wrap the same underlying runner and always apply on drift. Pick `idempotentStep()` when the operation is the whole job of a step; use `idempotent()` from anywhere — inside a multi-action step, a hook, or a bare async function. Pick `checkStep()` when the step should respect the run-level check mode — `kici run --check` previews the drift without applying it.
|
|
5790
|
+
`idempotent()` and `idempotentStep()` wrap the same underlying runner and always apply on drift. Pick `idempotentStep()` when the operation is the whole job of a step; use `idempotent()` from anywhere — inside a multi-action step, a hook, or a bare async function. Pick `checkStep()` when the step should respect the run-level check mode — `kici run remote --check` previews the drift without applying it.
|
|
5675
5791
|
|
|
5676
5792
|
## `idempotent(options)`
|
|
5677
5793
|
|
|
@@ -5766,30 +5882,30 @@ export const setup = job('setup', {
|
|
|
5766
5882
|
|
|
5767
5883
|
## `checkStep(name, options)`
|
|
5768
5884
|
|
|
5769
|
-
The check-mode-aware sibling of `idempotentStep()`. It takes a closely related option shape, but behaves differently when a run is started in check mode (`kici run --check`):
|
|
5885
|
+
The check-mode-aware sibling of `idempotentStep()`. It takes a closely related option shape, but behaves differently when a run is started in check mode (`kici run remote --check`):
|
|
5770
5886
|
|
|
5771
|
-
| Factory | Behavior under `kici run --check`
|
|
5887
|
+
| Factory | Behavior under `kici run remote --check` |
|
|
5772
5888
|
| ---------------- | ----------------------------------------- |
|
|
5773
5889
|
| `idempotentStep` | always applies on drift |
|
|
5774
5890
|
| `checkStep` | reports drift, applies only in apply mode |
|
|
5775
5891
|
|
|
5776
|
-
Use `checkStep()` for deploy-style steps where you want a dry-run preview of pending changes before committing them, and `idempotentStep()` for steps that must always converge (for example inside a hook). A `checkStep()` desugars to the first-class step check facet (`check` / `summarize` / `run(ctx, drift)` / `whenInSync`), so it participates in run-level check mode automatically: `kici run --check` reports the drift and skips `apply`, `kici run --check --fail-on-drift` exits non-zero when drift is detected, and apply mode applies the change.
|
|
5892
|
+
Use `checkStep()` for deploy-style steps where you want a dry-run preview of pending changes before committing them, and `idempotentStep()` for steps that must always converge (for example inside a hook). A `checkStep()` desugars to the first-class step check facet (`check` / `summarize` / `run(ctx, drift)` / `whenInSync`), so it participates in run-level check mode automatically: `kici run remote --check` reports the drift and skips `apply`, `kici run remote --check --fail-on-drift` exits non-zero when drift is detected, and apply mode applies the change.
|
|
5777
5893
|
|
|
5778
5894
|
### Parameters
|
|
5779
5895
|
|
|
5780
|
-
| Name | Type | Required | Description
|
|
5781
|
-
| ----------------- | ------------------------------------------- | -------- |
|
|
5782
|
-
| `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines.
|
|
5783
|
-
| `check` | `(ctx) => Promise<TDrift \| null>` | Yes | Read-only inspection. Return `null` when the system is already in the desired state.
|
|
5784
|
-
| `apply` | `(ctx, drift: TDrift) => Promise<TApplied>` | Yes | Brings the system to the desired state. Runs only in apply mode (skipped under `kici run --check`). |
|
|
5785
|
-
| `summarize` | `(drift: TDrift) => string` | Yes | Human-readable summary of what `apply()` would do; shown in check-mode drift output.
|
|
5786
|
-
| `whenInSync` | `(ctx) => Promise<TInSync>` | No | Runs when `check()` returned `null` (already in sync).
|
|
5787
|
-
| `outputs` | `OutputSchema` | No | Zod schema validating the step's outputs at runtime.
|
|
5788
|
-
| `continueOnError` | `boolean` | No | When true, the job proceeds even if this step fails.
|
|
5789
|
-
| `timeout` | `number` | No | Step-level timeout in milliseconds.
|
|
5790
|
-
| `retry` | `number \| RetryConfig` | No | Retry policy for the step; `retry: N` is shorthand for `{ maxAttempts: N }`.
|
|
5791
|
-
| `cache` | `CacheInput` | No | Declarative cache restored before the step and saved after it succeeds.
|
|
5792
|
-
| `rules` | `Rule[]` | No | Step-level conditional rules, evaluated agent-side.
|
|
5896
|
+
| Name | Type | Required | Description |
|
|
5897
|
+
| ----------------- | ------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
|
|
5898
|
+
| `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines. |
|
|
5899
|
+
| `check` | `(ctx) => Promise<TDrift \| null>` | Yes | Read-only inspection. Return `null` when the system is already in the desired state. |
|
|
5900
|
+
| `apply` | `(ctx, drift: TDrift) => Promise<TApplied>` | Yes | Brings the system to the desired state. Runs only in apply mode (skipped under `kici run remote --check`). |
|
|
5901
|
+
| `summarize` | `(drift: TDrift) => string` | Yes | Human-readable summary of what `apply()` would do; shown in check-mode drift output. |
|
|
5902
|
+
| `whenInSync` | `(ctx) => Promise<TInSync>` | No | Runs when `check()` returned `null` (already in sync). |
|
|
5903
|
+
| `outputs` | `OutputSchema` | No | Zod schema validating the step's outputs at runtime. |
|
|
5904
|
+
| `continueOnError` | `boolean` | No | When true, the job proceeds even if this step fails. |
|
|
5905
|
+
| `timeout` | `number` | No | Step-level timeout in milliseconds. |
|
|
5906
|
+
| `retry` | `number \| RetryConfig` | No | Retry policy for the step; `retry: N` is shorthand for `{ maxAttempts: N }`. |
|
|
5907
|
+
| `cache` | `CacheInput` | No | Declarative cache restored before the step and saved after it succeeds. |
|
|
5908
|
+
| `rules` | `Rule[]` | No | Step-level conditional rules, evaluated agent-side. |
|
|
5793
5909
|
|
|
5794
5910
|
Everything from `outputs` down is a plain [`step()` option](https://docs.kici.dev/user/sdk/core/) forwarded to the underlying step. The three step options `checkStep` does **not** accept are `onCancel`, `cleanup`, and `approval`.
|
|
5795
5911
|
|
|
@@ -5823,7 +5939,7 @@ export const deploy = job('deploy', {
|
|
|
5823
5939
|
});
|
|
5824
5940
|
```
|
|
5825
5941
|
|
|
5826
|
-
Run `kici run --check` against this workflow to see the drift summary without touching DNS; run it without `--check` to apply.
|
|
5942
|
+
Run `kici run remote --check` against this workflow to see the drift summary without touching DNS; run it without `--check` to apply.
|
|
5827
5943
|
|
|
5828
5944
|
## Worked example: create-if-missing returning a resource id
|
|
5829
5945
|
|
|
@@ -6586,6 +6702,8 @@ const deploy = job('deploy', {
|
|
|
6586
6702
|
|
|
6587
6703
|
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).
|
|
6588
6704
|
|
|
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/provenance/#id-token-claims-and-cloud-trust-policies) for the full claim table and a worked AWS policy.
|
|
6706
|
+
|
|
6589
6707
|
```typescript
|
|
6590
6708
|
const publish = job('publish', {
|
|
6591
6709
|
steps: [
|
|
@@ -6625,6 +6743,8 @@ interface HostInventoryEntry {
|
|
|
6625
6743
|
|
|
6626
6744
|
Two dimensions describe a host. **Labels** are flat strings used for grouping and targeting (the same labels `runsOn` / `runsOnAll` match). **Properties** are typed host-vars (`string | number | boolean`) — the place for facts like `region`, `cores`, or `gpu`. A host reports its own properties via the agent's `KICI_PROPERTIES` config, and an operator can pre-declare them with `kici-admin host declare --prop key=value`; the two are shallow-merged (agent-reported keys win).
|
|
6627
6745
|
|
|
6746
|
+
`labels` and `hostname` come back lowercase — KiCI folds both, so a pool declaring `Docker` reports `docker`. Compare against a lowercase value: `h.labels.includes('gpu')`, not `h.labels.includes('GPU')`. A label selector passed to `query()` folds too, so `{ include: [['GPU']] }` matches. `agentId` and `properties` keep their case.
|
|
6747
|
+
|
|
6628
6748
|
```typescript
|
|
6629
6749
|
// All hosts:
|
|
6630
6750
|
const all = await ctx.kici.inventory.query();
|
|
@@ -7320,7 +7440,7 @@ triggered, or the repo had no lock file at that commit.
|
|
|
7320
7440
|
source-registration mismatch).
|
|
7321
7441
|
2. **Did anything match?** Run `kici preview push --branch <your-branch>`
|
|
7322
7442
|
against your workflow. If it reports no matching workflow, your triggers don't
|
|
7323
|
-
cover that event/branch — the push was delivered and
|
|
7443
|
+
cover that event/branch — the push was delivered and matched nothing.
|
|
7324
7444
|
3. **Was there a lock file?** A repository with **no** `kici.lock.json` at the
|
|
7325
7445
|
pushed commit produces no run and is not an error. Confirm the lock file is
|
|
7326
7446
|
committed and current (see [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift)).
|
|
@@ -7639,7 +7759,7 @@ KiCI uses a **two-artifact model**: TypeScript workflows are the source of truth
|
|
|
7639
7759
|
## Why the lock file matters
|
|
7640
7760
|
|
|
7641
7761
|
- **Orchestrator** fetches the lock file at the commit SHA and uses it to evaluate triggers and to look up the cached `.kici/` source tarball + `node_modules` tarball. It never runs your TypeScript.
|
|
7642
|
-
- **Agents** download the cached source tarball (or, on cold cache, the build agent clones + packs it), register the shared TypeScript loader hook, and dynamic-`import()` the workflow `.ts` directly. The lock file's per-workflow `contentHash` identifies the expected
|
|
7762
|
+
- **Agents** download the cached source tarball (or, on cold cache, the build agent clones + packs it), register the shared TypeScript loader hook, and dynamic-`import()` the workflow `.ts` directly. The lock file's per-workflow `contentHash` identifies the expected contents of the whole `.kici/` directory and is verified against the extracted source before any step runs. The tarball's own bytes are verified against the digest the orchestrator dispatched, and the restored tree **replaces** `.kici/` rather than being unpacked over it, so a file you deleted does not survive a cache hit.
|
|
7643
7763
|
|
|
7644
7764
|
If you change a workflow file (`.ts`) but do **not** regenerate and commit the lock file, the repo at that commit has **drift**: the lock file no longer matches the source. Triggers and cache keys can be wrong, and runs can fail with a clear “stale lock file” error once the agent verifies the hash.
|
|
7645
7765
|
|
|
@@ -7647,37 +7767,38 @@ If you change a workflow file (`.ts`) but do **not** regenerate and commit the l
|
|
|
7647
7767
|
|
|
7648
7768
|
The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
|
|
7649
7769
|
|
|
7650
|
-
| Field | Description
|
|
7651
|
-
| ------------------ |
|
|
7652
|
-
| `schemaVersion` | Lock file schema version, stamped by the compiler that produced the lock. Incremented on every format change. The orchestrator accepts a range of versions — see [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window) — rather than requiring an exact match.
|
|
7653
|
-
| `minReaderVersion` | The oldest orchestrator schema version that can read this lock (the newest breaking version at compile time). An orchestrator whose own schema is below this rejects the lock and asks you to upgrade it. Omitted on locks compiled before the compatibility window existed. See [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window).
|
|
7654
|
-
| `source` | Reference to the source file and export (e.g., `{ file: '.kici/workflows/ci.ts', export: '#default' }`).
|
|
7655
|
-
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes.
|
|
7656
|
-
| `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists.
|
|
7657
|
-
| `
|
|
7770
|
+
| Field | Description |
|
|
7771
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
7772
|
+
| `schemaVersion` | Lock file schema version, stamped by the compiler that produced the lock. Incremented on every format change. The orchestrator accepts a range of versions — see [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window) — rather than requiring an exact match. |
|
|
7773
|
+
| `minReaderVersion` | The oldest orchestrator schema version that can read this lock (the newest breaking version at compile time). An orchestrator whose own schema is below this rejects the lock and asks you to upgrade it. Omitted on locks compiled before the compatibility window existed. See [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window). |
|
|
7774
|
+
| `source` | Reference to the source file and export (e.g., `{ file: '.kici/workflows/ci.ts', export: '#default' }`). |
|
|
7775
|
+
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
|
|
7776
|
+
| `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
|
|
7777
|
+
| `siblingsDigest` | SHA-256 over the git-tracked source of every in-repo `workspace:` / `file:` / `link:` / `portal:` sibling package `.kici` depends on, transitively. Part of the dependency cache key alongside `lockfileHash`, because editing a sibling's source moves no package manager lockfile. Omitted when `.kici` depends on no in-repo package, which is the common case. |
|
|
7778
|
+
| `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
|
|
7658
7779
|
|
|
7659
7780
|
Each workflow entry includes:
|
|
7660
7781
|
|
|
7661
|
-
| Field | Description
|
|
7662
|
-
| ---------------------- |
|
|
7663
|
-
| `name` | Workflow name.
|
|
7664
|
-
| `source` | Per-workflow source file and export reference.
|
|
7665
|
-
| `contentHash` | SHA-256 of the
|
|
7666
|
-
| `compileSchemaVersion` | Compiler schema version used when computing `contentHash` (currently `
|
|
7667
|
-
| `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching).
|
|
7668
|
-
| `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, contexts, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.).
|
|
7669
|
-
| `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized.
|
|
7670
|
-
| `description` | Optional workflow description.
|
|
7671
|
-
| `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles).
|
|
7672
|
-
| `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering.
|
|
7673
|
-
| `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch.
|
|
7674
|
-
| `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/).
|
|
7675
|
-
| `installEnv` | Extra qualified secret refs (`<context>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/).
|
|
7676
|
-
| `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/).
|
|
7677
|
-
| `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline.
|
|
7678
|
-
| `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/).
|
|
7679
|
-
| `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter).
|
|
7680
|
-
| Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`.
|
|
7782
|
+
| Field | Description |
|
|
7783
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7784
|
+
| `name` | Workflow name. |
|
|
7785
|
+
| `source` | Per-workflow source file and export reference. |
|
|
7786
|
+
| `contentHash` | SHA-256 of a digest over the whole `.kici/` directory mixed with `compileSchemaVersion` (and an `assetDigest` of declared `hashFiles` when present): `SHA-256(compileSchemaVersion + ":" + treeDigest [+ "\0" + assetDigest])`. The tree digest covers every file under `.kici/` except the paths declared in `.kici/.kiciignore` — see [files the content hash skips](https://docs.kici.dev/user/lock-file-and-drift/#files-the-content-hash-skips-kicikiciignore). Paths are sorted and line endings normalized. The orchestrator uses this as the source-tarball cache key and the agent re-computes it against the extracted tree to detect drift. |
|
|
7787
|
+
| `compileSchemaVersion` | Compiler schema version used when computing `contentHash` (currently `7`). The hash input is line-ending-normalized (CRLF → LF) so a lock file produced on Linux matches the agent's hash on Windows where Git's `core.autocrlf=true` rewrites checked-out text to CRLF. Bumping the schema version invalidates every existing source cache entry even if source is unchanged, which is the correct behavior when the compile-time or runtime contract changes. |
|
|
7788
|
+
| `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching). |
|
|
7789
|
+
| `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, contexts, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
|
|
7790
|
+
| `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
|
|
7791
|
+
| `description` | Optional workflow description. |
|
|
7792
|
+
| `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles). |
|
|
7793
|
+
| `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
|
|
7794
|
+
| `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
|
|
7795
|
+
| `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/). |
|
|
7796
|
+
| `installEnv` | Extra qualified secret refs (`<context>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/). |
|
|
7797
|
+
| `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
|
|
7798
|
+
| `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
|
|
7799
|
+
| `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/). |
|
|
7800
|
+
| `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter). |
|
|
7801
|
+
| Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
|
|
7681
7802
|
|
|
7682
7803
|
Step entries carry their own capability flags, so the orchestrator can reason about a step without loading your TypeScript:
|
|
7683
7804
|
|
|
@@ -7758,9 +7879,40 @@ kici compile --check
|
|
|
7758
7879
|
|
|
7759
7880
|
This validates all workflows and generates the lock file in memory without writing it. If any workflow has syntax errors or invalid configuration, the command exits non-zero. Pair this with the agent-side hash verification (below) for full drift detection -- `--check` catches broken source, while the agent catches source-lock-file mismatches at run time.
|
|
7760
7881
|
|
|
7882
|
+
## Files the content hash skips (`.kici/.kiciignore`)
|
|
7883
|
+
|
|
7884
|
+
The per-workflow content hash covers everything under `.kici/` except the paths declared in `.kici/.kiciignore`. `kici init` writes that file for you with this default set:
|
|
7885
|
+
|
|
7886
|
+
```
|
|
7887
|
+
node_modules/
|
|
7888
|
+
types/
|
|
7889
|
+
.npmrc
|
|
7890
|
+
package-lock.json
|
|
7891
|
+
pnpm-lock.yaml
|
|
7892
|
+
kici.lock.json
|
|
7893
|
+
```
|
|
7894
|
+
|
|
7895
|
+
Every entry except `kici.lock.json` names something KiCI itself regenerates. The agent installs your workflow's dependencies before it re-checks the hash, and that install rewrites `package-lock.json`, `pnpm-lock.yaml`, `.npmrc` and `node_modules/`; `kici compile` refreshes `types/` after it has already hashed the tree. Hashing any of them would make the hash change on every run, and the drift gate would reject work that never changed.
|
|
7896
|
+
|
|
7897
|
+
`kici.lock.json` is different: the hash is written **into** that file, so hashing it would make it an input to itself. It stays excluded whatever your `.kiciignore` says.
|
|
7898
|
+
|
|
7899
|
+
Patterns are gitignore-style and are matched relative to `.kici/`. A trailing `/` matches a directory and everything beneath it, a bare name matches at any depth, and a pattern containing a slash is anchored at `.kici/`.
|
|
7900
|
+
|
|
7901
|
+
One rule differs from `git`: **a symlink to a directory counts as a directory**. So `node_modules/` covers a `.kici/node_modules` that is a symlink into a shared dependency tree, where `git` would treat that link as a file. The exclusion means "skip the dependency tree, whatever shape it takes on disk". Hashing the link instead produced a hash your build agent could not reproduce, because its own dependency install always writes a real directory there.
|
|
7902
|
+
|
|
7903
|
+
A symlink the exclusions do **not** cover is still hashed — as its link target, not as the bytes behind it. The source tarball has to carry that link unchanged for the agent to agree. So `kici compile` warns about a link it cannot carry: one whose target is absolute (extraction strips the leading `/`), or whose target points outside `.kici/`'s parent (extraction drops the link). Point the link inside `.kici/`, replace it with the files it names, or list it in `.kiciignore`.
|
|
7904
|
+
|
|
7905
|
+
:::caution[The file replaces the defaults — it does not add to them]
|
|
7906
|
+
When `.kici/.kiciignore` exists, it **is** the exclusion list. A one-line file excludes one path and re-includes everything else, `package-lock.json` included. `kici compile` warns when your file omits a path a run rewrites, and names both the path and the instability it causes. Delete the file to fall back to the defaults.
|
|
7907
|
+
:::
|
|
7908
|
+
|
|
7909
|
+
`.kiciignore` is itself covered by the hash. Which files define a workflow's identity is part of that identity, so editing the file forces a recompile — and nobody can change what a lock file attests to without changing the lock file.
|
|
7910
|
+
|
|
7911
|
+
> **Not the repo-root `.kiciignore`.** A `.kiciignore` at the root of your repository is a separate, unrelated file: it selects which working-tree files `kici run remote` uploads. Only the one inside `.kici/` affects the content hash.
|
|
7912
|
+
|
|
7761
7913
|
## Extra files in the content hash (`hashFiles`)
|
|
7762
7914
|
|
|
7763
|
-
|
|
7915
|
+
A helper the workflow imports from `.kici/lib/` is already covered, so editing it invalidates the cache on its own. If your workflow depends on files **outside** `.kici/` -- configuration files, scripts, Dockerfiles, etc. -- changes to those files will **not** invalidate the cache unless you declare them.
|
|
7764
7916
|
|
|
7765
7917
|
Use the `hashFiles` option on a workflow to include additional paths or glob patterns (relative to the repo root) in the content hash:
|
|
7766
7918
|
|
|
@@ -7771,14 +7923,14 @@ export default workflow('deploy', {
|
|
|
7771
7923
|
});
|
|
7772
7924
|
```
|
|
7773
7925
|
|
|
7774
|
-
When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" +
|
|
7926
|
+
When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" + treeDigest + "\0" + assetDigest)` where `assetDigest` is a deterministic encoding of the resolved file paths and their contents. This busts the source-tarball cache and forces the build agent to pack and upload a fresh tarball. The resolved file paths are recorded in the lock file under `resolvedHashFiles` so the agent can verify without re-discovering the workflow.
|
|
7775
7927
|
|
|
7776
7928
|
## Agent-side safety net
|
|
7777
7929
|
|
|
7778
7930
|
If drift still occurs (e.g. someone committed only the `.ts` change), the agent detects it at run time before any step runs:
|
|
7779
7931
|
|
|
7780
|
-
- After extracting the `.kici/` source tarball (or loading source from a `git clone` on the build path), the agent
|
|
7781
|
-
- If the orchestrator sent a `contentHash` (from the lock file) and the computed hash does **not** match, the agent fails the run with an error like: **lock file is out of date** (workflow source changed without regenerating the lock file). The error includes the baked agent `@kici-dev/sdk` version + bundle hash so operators can debug cross-host compile mismatches.
|
|
7932
|
+
- After extracting the `.kici/` source tarball (or loading source from a `git clone` on the build path), the agent walks the whole extracted `.kici/` tree and re-computes `contentHash = SHA-256(compileSchemaVersion + ":" + treeDigest [+ "\0" + assetDigest])` using the same implementation as the compiler. Because it covers the tree, an edit to any file the workflow imports is caught, not just an edit to the entry file.
|
|
7933
|
+
- If the orchestrator sent a `contentHash` (from the lock file) and the computed hash does **not** match, the agent fails the run with an error like: **lock file is out of date** (workflow source changed without regenerating the lock file). The error includes the baked agent `@kici-dev/sdk` version + bundle hash so operators can debug cross-host compile mismatches. When the hashed tree carries symlinks, the error names them too — recompiling cannot reconcile a link the tarball omits or extraction rewrites, so the usual remedy would loop.
|
|
7782
7934
|
|
|
7783
7935
|
So even without a pre-commit or CI check, a stale lock file will cause the run to fail with a clear message instead of running with the wrong workflow.
|
|
7784
7936
|
|
|
@@ -7790,6 +7942,7 @@ So even without a pre-commit or CI check, a stale lock file will cause the run t
|
|
|
7790
7942
|
| Catch drift before commit | Install a pre-commit hook with `kici hook install`. |
|
|
7791
7943
|
| Catch broken source in CI | Run `kici compile --check` in CI. |
|
|
7792
7944
|
| Bust cache on external files | Add `hashFiles: ['config.json']` to include non-workflow files in the content hash. |
|
|
7945
|
+
| Skip a path inside `.kici/` | List it in `.kici/.kiciignore` — remember the file replaces the defaults. |
|
|
7793
7946
|
| Fail fast when drift remains | Rely on the agent’s hash verification when it compiles from source. |
|
|
7794
7947
|
|
|
7795
7948
|
## See also
|
|
@@ -8959,6 +9112,12 @@ If you belong to a single organization, the org is resolved automatically. If
|
|
|
8959
9112
|
you belong to several, pass an `orgId` argument to any tool (use `list_orgs` to
|
|
8960
9113
|
find it).
|
|
8961
9114
|
|
|
9115
|
+
Only an **active** membership counts: an organization you have been suspended
|
|
9116
|
+
in, one that has been disabled, and one that has been deleted are all skipped.
|
|
9117
|
+
So a single active membership alongside a disabled one still resolves
|
|
9118
|
+
automatically, and a tool call against an organization you are suspended in is
|
|
9119
|
+
refused with the same message the dashboard gives.
|
|
9120
|
+
|
|
8962
9121
|
### Limits and pagination
|
|
8963
9122
|
|
|
8964
9123
|
The MCP server applies a few bounds so an agent loop can't overwhelm the shared
|
|
@@ -9048,7 +9207,7 @@ you hold, so an agent cannot escalate beyond its creator.
|
|
|
9048
9207
|
|
|
9049
9208
|
**Repository scope comes along too.** If your role is restricted to a set of
|
|
9050
9209
|
repositories, an agent token you mint is restricted to the same set. Runs
|
|
9051
|
-
outside it are
|
|
9210
|
+
outside it are not there: they are filtered out of `list_runs`,
|
|
9052
9211
|
`cancel_runs_by_branch` skips them, and naming one directly answers "not found"
|
|
9053
9212
|
— the same answer a run id that does not exist gets, so an agent cannot use the
|
|
9054
9213
|
tools to discover which repositories it is missing. Your organization's audit
|
|
@@ -9291,14 +9450,18 @@ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMid
|
|
|
9291
9450
|
|
|
9292
9451
|
### Configurable surfaces
|
|
9293
9452
|
|
|
9294
|
-
The dashboard is a browser SPA on top of the same `/api/v1/*` surface, so
|
|
9453
|
+
The dashboard is a browser SPA on top of the same `/api/v1/*` surface, so nearly everything you can configure in the dashboard you can configure over HTTP. The exception is a short list of routes that only a **browser session** may call, marked † below. The mounted route groups include:
|
|
9295
9454
|
|
|
9296
|
-
- **Auth & identity:** `/cli/exchange-token
|
|
9297
|
-
- **Org & membership:** `/orgs
|
|
9455
|
+
- **Auth & identity:** `/cli/exchange-token`†, `/pats`, `/user`, `/identity-links`, `/identity-links/:id` (DELETE)†, `/github-oauth`, `/auth/github/link`†, `/invites`, `/invites/pending`, `/invites/:inviteId/{accept,decline}`†
|
|
9456
|
+
- **Org & membership:** `/orgs` (POST)†, `/orgs/:customerId`, `/orgs/:customerId/{members,roles,api-keys,orchestrator-keys,service-accounts,billing,trust-policies}`
|
|
9298
9457
|
- **Workflows & runs:** `/orgs/:customerId/{runs,registrations,workflows,held-runs,contexts,secrets,global-workflows}`
|
|
9299
9458
|
- **Webhooks & event log:** `/orgs/:customerId/{sources,webhook-endpoints,event-log}`
|
|
9300
9459
|
- **Diagnostics & activity:** `/orgs/:customerId/{diagnostics,activity,access-log}`
|
|
9301
9460
|
|
|
9461
|
+
**† Browser session only.** These routes answer **403** with `This endpoint requires an interactive login` to a `kici_pat_`, `kici_sk_` or `kici_sa_` token. They are the routes that create an organization, join or decline one, unlink a provider identity, start a GitHub account link, and exchange your session for a personal access token. Each mints a credential wider than the credential presenting it, or changes which organizations and provider identities your account reaches. A `kici_sk_` or `kici_sa_` token is bound to one organization and one permission set, so letting it take those actions would hand it access it was never granted. Do them in the dashboard, or with `kici login`, which runs the browser flow for you.
|
|
9462
|
+
|
|
9463
|
+
`POST /pats` is the one route in between: it accepts a browser session **or** an existing `kici_pat_` (so `kici pat create` keeps working), and refuses `kici_sk_` and `kici_sa_`. A token minted from another token can never be wider than that token. The new token is capped by your own permissions **and** by the scopes of the token you called with, whether or not you pass `permissions` explicitly. Mint from an unscoped session when you need a broader token.
|
|
9464
|
+
|
|
9302
9465
|
The full route tree is the source of truth — every method, request schema, and response schema is enumerated server-side. There is currently no auto-generated OpenAPI spec; the typed `DashboardApiType` export is the canonical contract for TypeScript clients.
|
|
9303
9466
|
|
|
9304
9467
|
### Calling the API
|
|
@@ -9317,11 +9480,16 @@ curl -sS \
|
|
|
9317
9480
|
|
|
9318
9481
|
**Browser console (after dashboard login):**
|
|
9319
9482
|
|
|
9483
|
+
Mint a personal access token and pass it explicitly. Do not script against the
|
|
9484
|
+
dashboard's own session token. That token is short-lived and tied to your
|
|
9485
|
+
identity-provider session, so anything built on it stops working at the next
|
|
9486
|
+
renewal or sign-out.
|
|
9487
|
+
|
|
9320
9488
|
```js
|
|
9321
|
-
|
|
9322
|
-
const
|
|
9489
|
+
// kici pat create --name console --expires-in-days 1 → prints the token
|
|
9490
|
+
const token = 'kici_pat_...';
|
|
9323
9491
|
const res = await fetch('/<deployment-slug>/api/v1/orgs/<your-org-id>/runs?limit=5', {
|
|
9324
|
-
headers: { Authorization: `Bearer ${
|
|
9492
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
9325
9493
|
});
|
|
9326
9494
|
console.log(await res.json());
|
|
9327
9495
|
```
|
|
@@ -9346,6 +9514,12 @@ The CLI stores authentication data in `~/.kici/config` with `0600` permissions (
|
|
|
9346
9514
|
- Routing key for webhook source identification
|
|
9347
9515
|
- API key, when you logged in with `--token`
|
|
9348
9516
|
|
|
9517
|
+
The web dashboard holds none of these. It keeps only the tokens of your current
|
|
9518
|
+
sign-in, in browser storage for the tab's origin. It does not request offline
|
|
9519
|
+
access, so those tokens die with your identity-provider session instead of
|
|
9520
|
+
staying valid for weeks. Its origin also serves a Content-Security-Policy that
|
|
9521
|
+
restricts which scripts run and which hosts the page may contact.
|
|
9522
|
+
|
|
9349
9523
|
## Troubleshooting
|
|
9350
9524
|
|
|
9351
9525
|
### Browser doesn't open
|
|
@@ -9550,10 +9724,14 @@ kici init --private-registry https://npm.pkg.github.com/ \
|
|
|
9550
9724
|
types/ # Directory for generated type declarations (kici types)
|
|
9551
9725
|
package.json # Dependencies (@kici-dev/sdk)
|
|
9552
9726
|
tsconfig.json # TypeScript configuration (includes types/**/*.d.ts)
|
|
9727
|
+
.gitignore # Keeps the generated types/ declarations untracked
|
|
9728
|
+
.kiciignore # Paths the workflow content hash does not cover
|
|
9553
9729
|
AGENTS.md # LLM authoring context (skip with --no-agents-md)
|
|
9554
9730
|
.kiciignore # Default exclusion patterns for test uploads
|
|
9555
9731
|
```
|
|
9556
9732
|
|
|
9733
|
+
The two `.kiciignore` files are unrelated. The one inside `.kici/` declares which paths the per-workflow content hash skips — see [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/#files-the-content-hash-skips-kicikiciignore). The one at the repo root selects which working-tree files a remote run uploads. Neither is overwritten when it already exists.
|
|
9734
|
+
|
|
9557
9735
|
`AGENTS.md` is written by default (the interactive prompt defaults to yes, and CI / non-interactive runs write it). An existing `.kici/AGENTS.md` is never overwritten, so hand edits survive a re-run.
|
|
9558
9736
|
|
|
9559
9737
|
In interactive mode (TTY), `kici init` prompts you to:
|
|
@@ -10094,6 +10272,14 @@ to see it. The bundle holds your CLI, Node and orchestrator versions, your
|
|
|
10094
10272
|
redacted configuration, and your project's workflow and lock-file state. With
|
|
10095
10273
|
`--run` it also holds the failing run's detail and logs.
|
|
10096
10274
|
|
|
10275
|
+
For every orchestrator the probe returned the bundle also records where that
|
|
10276
|
+
orchestrator's own config files live — the paths only, never the contents. An
|
|
10277
|
+
orchestrator that is offline is still listed, with no paths: the CLI reads them
|
|
10278
|
+
from the live connection, so a disconnected one has none to report.
|
|
10279
|
+
Each path is a host path as that orchestrator sees it. A container deployment
|
|
10280
|
+
names a file on the container host, which is not a file you can open from the
|
|
10281
|
+
machine that read the bundle.
|
|
10282
|
+
|
|
10097
10283
|
```bash
|
|
10098
10284
|
kici report [options]
|
|
10099
10285
|
```
|
|
@@ -11245,7 +11431,7 @@ You declare a gate in your workflow with `approval`. It is available at three le
|
|
|
11245
11431
|
- **Job** — hold the job before any of its steps run.
|
|
11246
11432
|
- **Workflow** — hold the whole run before any job is dispatched.
|
|
11247
11433
|
|
|
11248
|
-
A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-
|
|
11434
|
+
A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-when-drift) below.
|
|
11249
11435
|
|
|
11250
11436
|
Approvers are named as **teams** and **users**. A team is an operator-defined group of org members; your workflow code may name a team but can never change its membership, which is what makes a team clause a real gate rather than a suggestion. See [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) for how operators define teams, the approval queue, and expiry; see [the architecture overview](https://docs.kici.dev/architecture/approvals/) for how a hold is evaluated and resumed.
|
|
11251
11437
|
|
|
@@ -11316,7 +11502,7 @@ approval: {
|
|
|
11316
11502
|
|
|
11317
11503
|
| Field | Type | Description |
|
|
11318
11504
|
| ----------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11319
|
-
| `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-
|
|
11505
|
+
| `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-when-drift). |
|
|
11320
11506
|
| `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
|
|
11321
11507
|
| `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
|
|
11322
11508
|
| `timeout` | `number` | Per-gate expiry in **seconds**, overriding the org default. Must be a **positive integer** number of seconds; a non-positive or non-finite value is rejected at compile time. On expiry the element is rejected. |
|
|
@@ -11495,7 +11681,7 @@ Available rules:
|
|
|
11495
11681
|
- **Branch restrictions** — only allow specific branches to deploy.
|
|
11496
11682
|
- **Required reviewer approvals** — gate the run on human sign-off.
|
|
11497
11683
|
- **Wait timers** — delay execution for a fixed period.
|
|
11498
|
-
- **Concurrency limits** —
|
|
11684
|
+
- **Concurrency limits** — cap how many jobs run against the context at once.
|
|
11499
11685
|
|
|
11500
11686
|
<!-- /help:contexts-protection -->
|
|
11501
11687
|
|
|
@@ -11584,7 +11770,7 @@ job('deploy', {
|
|
|
11584
11770
|
|
|
11585
11771
|
**Skip-on-test (allow-and-warn).** On a test or local run (`kici run remote`, `kici run <event> --local`), a bound context never rejects the run. Any bound context 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 context(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 `contexts: ['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 context 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/).
|
|
11586
11772
|
|
|
11587
|
-
**Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context)
|
|
11773
|
+
**Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
|
|
11588
11774
|
|
|
11589
11775
|
**Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-context binding: a bound context that does not exist, a disabled one, or two contexts 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 contexts, and the rule — for example `unsatisfiable context binding: job 'deploy' binds contexts [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound contexts)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
|
|
11590
11776
|
|
|
@@ -11787,6 +11973,36 @@ The children of a matrix job count individually against the limit. A three-child
|
|
|
11787
11973
|
matrix bound to a context with a limit of two dispatches two children and applies
|
|
11788
11974
|
the strategy above to the third.
|
|
11789
11975
|
|
|
11976
|
+
A job counts against the limit from the moment it is dispatched until it
|
|
11977
|
+
finishes. It does not have to reach an agent first.
|
|
11978
|
+
|
|
11979
|
+
#### What the limit does and does not guarantee
|
|
11980
|
+
|
|
11981
|
+
A context concurrency limit is a **throughput control**. Treat it as a cap on
|
|
11982
|
+
how much work runs at once, not as a lock.
|
|
11983
|
+
|
|
11984
|
+
Two events that arrive in the same instant read the limit before either job is
|
|
11985
|
+
recorded, so each can be admitted. The window is short — the time between one
|
|
11986
|
+
read and one write — but it is real, and it grows with the number of
|
|
11987
|
+
orchestrator processes serving the context.
|
|
11988
|
+
|
|
11989
|
+
When a job must never run beside another copy of itself — a production deploy,
|
|
11990
|
+
a database migration — declare a workflow-level
|
|
11991
|
+
[concurrency group](https://docs.kici.dev/user/concurrency/) as well:
|
|
11992
|
+
|
|
11993
|
+
```typescript
|
|
11994
|
+
export default workflow('deploy', {
|
|
11995
|
+
on: push({ branches: ['main'] }),
|
|
11996
|
+
concurrency: { group: () => 'deploy-prod', max: 1 },
|
|
11997
|
+
jobs: [/* ... */],
|
|
11998
|
+
});
|
|
11999
|
+
```
|
|
12000
|
+
|
|
12001
|
+
That mechanism claims its slot inside a single database transaction, so two
|
|
12002
|
+
runs arriving together cannot both take it. The two are complementary: the
|
|
12003
|
+
context limit caps throughput across every workflow bound to the context, and
|
|
12004
|
+
the concurrency group serializes one workflow against itself.
|
|
12005
|
+
|
|
11790
12006
|
## Dashboard management
|
|
11791
12007
|
|
|
11792
12008
|
### Creating contexts
|
|
@@ -12093,7 +12309,7 @@ registries: [
|
|
|
12093
12309
|
## Security model
|
|
12094
12310
|
|
|
12095
12311
|
- **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.
|
|
12096
|
-
- **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips
|
|
12312
|
+
- **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips `npmRegistries`, `installEnvSecrets`, and a container job's [registry credentials](https://docs.kici.dev/user/container-jobs/#private-images) out of the dispatch. The install runs without auth and fails naturally on the first private dep, and a private base image fails to pull. A fork pull request cannot observe a registry token, even if a context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
|
|
12097
12313
|
- **Lifecycle scripts disabled.** Whenever a private registry is in scope, the agent runs the install with `--ignore-scripts` (npm, pnpm, and yarn classic alike; yarn berry gets the equivalent `enableScripts: false`). A malicious `preinstall` / `postinstall` hook in committed `package.json` cannot read the synthesized token env vars, even though they exist in the install subprocess. For a pnpm or yarn workspace, the agent builds your in-repo dependency closure as a separate step **after** the install's auth is torn down, so build scripts never see the tokens either.
|
|
12098
12314
|
- **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
|
|
12099
12315
|
- **Job-scoped env-var names.** The synthesized auth env var is `KICI_NPM_TOKEN_<jobIdShort>_<i>` where `jobIdShort` is the first 8 chars of the dispatched job id. The name is unguessable from outside the install subprocess and not reused across jobs.
|
|
@@ -12129,7 +12345,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
|
|
|
12129
12345
|
## See also
|
|
12130
12346
|
|
|
12131
12347
|
- [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
|
|
12132
|
-
- [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (
|
|
12348
|
+
- [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (branch restrictions, required reviewers, minimum trust) that the install gate inherits.
|
|
12133
12349
|
- [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
|
|
12134
12350
|
|
|
12135
12351
|
---
|
|
@@ -12247,6 +12463,107 @@ and the step never holds signing credentials — the orchestrator mints and sign
|
|
|
12247
12463
|
the token on the step's behalf from its own run records. Like `attestProvenance`,
|
|
12248
12464
|
it is only available inside a running job step.
|
|
12249
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
|
+
|
|
12250
12567
|
## Verifying an attestation
|
|
12251
12568
|
|
|
12252
12569
|
Verify a bundle with the `kici verify-attestation` command. It establishes the
|
|
@@ -12872,6 +13189,8 @@ workflow('test', {
|
|
|
12872
13189
|
|
|
12873
13190
|
When `max: 1` (default), runs are fully serialized within the group.
|
|
12874
13191
|
|
|
13192
|
+
`max` is enforced by the orchestrator's database, so the limit is cluster-wide and survives a restart. A run holding a slot keeps it across an orchestrator restart. Every orchestrator in a cluster counts against the same limit, so a group with `max: 1` runs one job at a time no matter which orchestrator dispatched it.
|
|
13193
|
+
|
|
12875
13194
|
## Group key examples
|
|
12876
13195
|
|
|
12877
13196
|
### Deploy per environment
|
|
@@ -12931,6 +13250,14 @@ When a workflow has both `concurrency` and `context` protection rules:
|
|
|
12931
13250
|
|
|
12932
13251
|
This means a run that passed approval won't need re-approval if it gets queued by concurrency.
|
|
12933
13252
|
|
|
13253
|
+
The two caps also differ in strength. A workflow-level concurrency group claims
|
|
13254
|
+
its slot inside a single database transaction, so two runs that arrive together
|
|
13255
|
+
cannot both take it. A context
|
|
13256
|
+
[concurrency limit](https://docs.kici.dev/user/contexts/#concurrency-limits) is a throughput control:
|
|
13257
|
+
simultaneous arrivals can each be admitted before either is recorded. Declare a
|
|
13258
|
+
concurrency group for anything that must never run beside itself, whatever the
|
|
13259
|
+
context limit says.
|
|
13260
|
+
|
|
12934
13261
|
## Cancelling queued runs
|
|
12935
13262
|
|
|
12936
13263
|
Queued runs can be cancelled before they start executing. The cancel request removes them from the queue immediately -- they don't go through the grace period since no step is running.
|
|
@@ -13141,6 +13468,17 @@ Store the secrets first with `kici-admin secret set`. Pasting a token straight
|
|
|
13141
13468
|
into the workflow is rejected when the workflow is defined, because a token
|
|
13142
13469
|
written into `.kici/` would be committed to your repository.
|
|
13143
13470
|
|
|
13471
|
+
The named context's protection rules run before the secret is read, exactly as
|
|
13472
|
+
they do for [git credentials](https://docs.kici.dev/user/patterns/git-credentials/#what-a-job-may-ask-for).
|
|
13473
|
+
A `prod:` reference from a branch the `prod` context restricts is refused, and
|
|
13474
|
+
the job is dispatched with no registry credentials — so a private image fails to
|
|
13475
|
+
pull rather than being pulled from a branch the context does not allow. The rule
|
|
13476
|
+
that refused it is named in your orchestrator's log, not in the run.
|
|
13477
|
+
|
|
13478
|
+
**An untrusted ref receives no registry credentials.** A fork pull request is
|
|
13479
|
+
dispatched without them, so a private base image fails to pull and a public one
|
|
13480
|
+
is unaffected. The run's reduced-privilege note says so.
|
|
13481
|
+
|
|
13144
13482
|
The username is not a secret, so you may write it directly:
|
|
13145
13483
|
|
|
13146
13484
|
```typescript
|
|
@@ -13150,6 +13488,30 @@ auth: { username: 'ci-bot', tokenSecret: 'prod:REGISTRY_TOKEN' }
|
|
|
13150
13488
|
Your orchestrator resolves these names at dispatch and sends only the resolved
|
|
13151
13489
|
credentials to the agent. The agent never reads your secret store.
|
|
13152
13490
|
|
|
13491
|
+
### Naming the registry
|
|
13492
|
+
|
|
13493
|
+
`auth` also takes a `registry` — the registry host the credentials belong to,
|
|
13494
|
+
such as `reg.internal:5000`.
|
|
13495
|
+
|
|
13496
|
+
With `image` it is optional, because KiCI reads the host off the image
|
|
13497
|
+
reference. With `dockerfile` it is **required**: the base image is named inside
|
|
13498
|
+
your Dockerfile, so there is nothing to read it from. A `dockerfile` job whose
|
|
13499
|
+
`auth` omits `registry` is refused when you define the workflow.
|
|
13500
|
+
|
|
13501
|
+
```typescript
|
|
13502
|
+
container: {
|
|
13503
|
+
dockerfile: '.kici/ci.Dockerfile',
|
|
13504
|
+
auth: {
|
|
13505
|
+
registry: 'reg.internal:5000',
|
|
13506
|
+
usernameSecret: 'prod:REGISTRY_USER',
|
|
13507
|
+
tokenSecret: 'prod:REGISTRY_TOKEN',
|
|
13508
|
+
},
|
|
13509
|
+
},
|
|
13510
|
+
```
|
|
13511
|
+
|
|
13512
|
+
With `dockerfile`, these credentials pull the Dockerfile's own `FROM` base — not
|
|
13513
|
+
a job image, since the job image is the one KiCI builds.
|
|
13514
|
+
|
|
13153
13515
|
### Credentials that only exist at run time
|
|
13154
13516
|
|
|
13155
13517
|
A token fetched during the run — from a cloud registry's login command, for
|
|
@@ -13300,10 +13662,11 @@ The KiCI CLI reads the following environment variables to customize its behavior
|
|
|
13300
13662
|
|
|
13301
13663
|
## Development
|
|
13302
13664
|
|
|
13303
|
-
| Variable
|
|
13304
|
-
|
|
|
13305
|
-
| `KICI_DEV`
|
|
13306
|
-
| `
|
|
13665
|
+
| Variable | Description | Default |
|
|
13666
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
13667
|
+
| `KICI_DEV` | Enable development mode. When `true`, uses prerelease-compatible version ranges (`>=0.0.1-0`) for dev dependencies and skips npm version resolution. | unset |
|
|
13668
|
+
| `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
|
+
| `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 |
|
|
13307
13670
|
|
|
13308
13671
|
## Local dev plane
|
|
13309
13672
|
|
|
@@ -13637,7 +14000,7 @@ job('provision', {
|
|
|
13637
14000
|
|
|
13638
14001
|
### Trust tiers on internal triggers
|
|
13639
14002
|
|
|
13640
|
-
An internally-triggered run resolves its [trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) from the trigger. The tier decides the run's cache scope, whether it may run a [Dockerfile build](https://docs.kici.dev/user/container-jobs/#who-may-build), whether it receives [install secrets](https://docs.kici.dev/user/private-registries/), and whether a `minimumTrust` context holds it.
|
|
14003
|
+
An internally-triggered run resolves its [trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) from the trigger. The tier decides the run's cache scope, whether it may run a [Dockerfile build](https://docs.kici.dev/user/container-jobs/#who-may-build), whether it receives [install secrets](https://docs.kici.dev/user/private-registries/), and whether a `minimumTrust` context holds it. It also decides whether the run's jobs receive [container-registry credentials](https://docs.kici.dev/user/container-jobs/#private-images) and their declared [git credentials](https://docs.kici.dev/user/patterns/git-credentials/#what-a-job-may-ask-for).
|
|
13641
14004
|
|
|
13642
14005
|
Four rules resolve the tier, and KiCI applies them in this order:
|
|
13643
14006
|
|
|
@@ -14164,7 +14527,7 @@ The registration log line names the organization it decided against, so a refusa
|
|
|
14164
14527
|
|
|
14165
14528
|
### Secrets are not available
|
|
14166
14529
|
|
|
14167
|
-
A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is
|
|
14530
|
+
A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
|
|
14168
14531
|
|
|
14169
14532
|
This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
|
|
14170
14533
|
|
|
@@ -15394,10 +15757,14 @@ kici-admin secret set <orgId> "__source__/<sourceId>" pat --value "<your-forgejo
|
|
|
15394
15757
|
Finally, configure the forge to deliver webhooks to:
|
|
15395
15758
|
|
|
15396
15759
|
```
|
|
15397
|
-
https://<platform-host>/webhook/<orgId>/generic/<
|
|
15760
|
+
https://<platform-host>/webhook/<orgId>/generic/<sourceId>
|
|
15398
15761
|
```
|
|
15399
15762
|
|
|
15400
|
-
with the same secret you passed to `--secret`.
|
|
15763
|
+
with the same secret you passed to `--secret`. `<orgId>` is your Platform
|
|
15764
|
+
organization id — the same value you passed to `--org`, which the source's
|
|
15765
|
+
routing key embeds — and `<sourceId>` is the id `source add` printed. The
|
|
15766
|
+
orchestrator's own ingest URL has the same shape but takes the source **name**
|
|
15767
|
+
in place of the id.
|
|
15401
15768
|
|
|
15402
15769
|
## SSH deploy key
|
|
15403
15770
|
|
|
@@ -15591,7 +15958,7 @@ interface LocalConfig {
|
|
|
15591
15958
|
|
|
15592
15959
|
### SharedConfig
|
|
15593
15960
|
|
|
15594
|
-
|
|
15961
|
+
Settings stored in the PostgreSQL `config_versions` table, written and read by the `/admin/config` routes and their `kici-admin config` commands:
|
|
15595
15962
|
|
|
15596
15963
|
```typescript
|
|
15597
15964
|
interface SharedConfig {
|
|
@@ -15635,11 +16002,13 @@ interface AppConfig {
|
|
|
15635
16002
|
|
|
15636
16003
|
### How they merge
|
|
15637
16004
|
|
|
16005
|
+
`resolveFullConfig()` takes a `LocalConfig` and an optional `SharedConfig` and merges them:
|
|
16006
|
+
|
|
15638
16007
|
```
|
|
15639
16008
|
defaults (getDefaults())
|
|
15640
16009
|
|
|
|
15641
16010
|
v
|
|
15642
|
-
SharedConfig (
|
|
16011
|
+
SharedConfig (argument) ──deepMerge──> merged layer 1+2
|
|
15643
16012
|
|
|
|
15644
16013
|
v
|
|
15645
16014
|
LocalConfig (from YAML) ──deepMerge──> merged layer 1+2+3
|
|
@@ -15656,29 +16025,48 @@ appConfigSchema.safeParse() ──validate──> typed AppConfig
|
|
|
15656
16025
|
|
|
15657
16026
|
The `deepMerge` function merges objects recursively, replaces arrays (does not merge item-by-item), and skips `undefined`/`null` source values (they do not override existing values).
|
|
15658
16027
|
|
|
16028
|
+
**The `SharedConfig` argument is `null` in the shipped wiring.** `ConfigReloader` is the only non-test caller of `resolveFullConfig()`, and it is constructed with `sharedStore: null`. So the DB layer is skipped and the effective chain is defaults → YAML → env. The `config_versions` table is read by the `/admin/config` write and inspection routes, by `kici-admin rotate-key`, and by the cluster join flow — never by a running orchestrator's own config.
|
|
16029
|
+
|
|
15659
16030
|
## Resolution chain
|
|
15660
16031
|
|
|
15661
|
-
###
|
|
16032
|
+
### Startup
|
|
16033
|
+
|
|
16034
|
+
`server.ts` and `standalone.ts` both call `loadConfig()`, which parses `KICI_*` environment variables against the flat schema in `config.ts`. No YAML file and no database row participates:
|
|
15662
16035
|
|
|
15663
16036
|
```
|
|
15664
|
-
|
|
15665
|
-
|
|
15666
|
-
|
|
15667
|
-
|
|
15668
|
-
|
|
15669
|
-
|
|
15670
|
-
|
|
15671
|
-
|
|
15672
|
-
|
|
15673
|
-
|
|
15674
|
-
Phase 2 (full merge):
|
|
15675
|
-
defaults -> DB -> YAML -> env
|
|
15676
|
-
|
|
|
15677
|
-
v
|
|
15678
|
-
resolveFullConfig() -> AppConfig
|
|
16037
|
+
Process start
|
|
16038
|
+
|
|
|
16039
|
+
v
|
|
16040
|
+
loadConfig() -> envDef.parse(process.env) -> AppConfig
|
|
16041
|
+
|
|
|
16042
|
+
v
|
|
16043
|
+
Connect to PostgreSQL, run migrations
|
|
16044
|
+
|
|
|
16045
|
+
v
|
|
16046
|
+
Start server (HTTP, WS, scaler, cluster)
|
|
15679
16047
|
```
|
|
15680
16048
|
|
|
15681
|
-
|
|
16049
|
+
The database URL therefore has to be an environment variable: the orchestrator needs it to reach PostgreSQL, and the shared config lives in PostgreSQL.
|
|
16050
|
+
|
|
16051
|
+
### Reload
|
|
16052
|
+
|
|
16053
|
+
`resolveLocalConfig()` and `resolveFullConfig()` run on the reload path, not at startup:
|
|
16054
|
+
|
|
16055
|
+
```
|
|
16056
|
+
SIGHUP / POST /admin/config/reload / kici-admin config reload
|
|
16057
|
+
|
|
|
16058
|
+
v
|
|
16059
|
+
resolveLocalConfig() -> YAML file + KICI_ env overlay
|
|
16060
|
+
|
|
|
16061
|
+
v
|
|
16062
|
+
resolveFullConfig(local, null) -> defaults -> YAML -> env -> AppConfig
|
|
16063
|
+
|
|
|
16064
|
+
v
|
|
16065
|
+
Hold databaseUrl, port, instanceId and storage at their startup values
|
|
16066
|
+
|
|
|
16067
|
+
v
|
|
16068
|
+
Atomic swap into ConfigReloader.currentConfig
|
|
16069
|
+
```
|
|
15682
16070
|
|
|
15683
16071
|
### Env var processing
|
|
15684
16072
|
|
|
@@ -15690,43 +16078,6 @@ Environment variables are processed in two stages:
|
|
|
15690
16078
|
|
|
15691
16079
|
Type coercion is applied based on known field types: numeric fields are parsed as numbers, boolean fields are compared against `"true"`, all others remain strings.
|
|
15692
16080
|
|
|
15693
|
-
## Two-phase bootstrap
|
|
15694
|
-
|
|
15695
|
-
```
|
|
15696
|
-
┌─────────────────┐
|
|
15697
|
-
│ Process Start │
|
|
15698
|
-
└────────┬────────┘
|
|
15699
|
-
│
|
|
15700
|
-
▼
|
|
15701
|
-
┌─────────────────┐
|
|
15702
|
-
│ Load YAML + │ resolveLocalConfig()
|
|
15703
|
-
│ Env Overrides │ -> databaseUrl, instanceId, port, mode
|
|
15704
|
-
└────────┬────────┘
|
|
15705
|
-
│
|
|
15706
|
-
▼
|
|
15707
|
-
┌─────────────────┐
|
|
15708
|
-
│ Connect to │ PostgreSQL
|
|
15709
|
-
│ Database │ Run migrations
|
|
15710
|
-
└────────┬────────┘
|
|
15711
|
-
│
|
|
15712
|
-
▼
|
|
15713
|
-
┌─────────────────┐
|
|
15714
|
-
│ Load Shared │ SharedConfigStore.getLatest()
|
|
15715
|
-
│ Config from DB │ -> decrypt -> SharedConfig
|
|
15716
|
-
└────────┬────────┘
|
|
15717
|
-
│
|
|
15718
|
-
▼
|
|
15719
|
-
┌─────────────────┐
|
|
15720
|
-
│ Full Merge │ resolveFullConfig(local, db, env)
|
|
15721
|
-
│ + Validate │ -> AppConfig
|
|
15722
|
-
└────────┬────────┘
|
|
15723
|
-
│
|
|
15724
|
-
▼
|
|
15725
|
-
┌─────────────────┐
|
|
15726
|
-
│ Start Server │ HTTP, WS, scaler, cluster
|
|
15727
|
-
└─────────────────┘
|
|
15728
|
-
```
|
|
15729
|
-
|
|
15730
16081
|
## DB schema
|
|
15731
16082
|
|
|
15732
16083
|
### config_versions table
|
|
@@ -15826,7 +16177,7 @@ flowchart TD
|
|
|
15826
16177
|
trigger --> execute["executeReload()<br/>boolean mutex"]
|
|
15827
16178
|
|
|
15828
16179
|
execute --> resolveLocal["resolveLocalConfig()"]
|
|
15829
|
-
execute --> getLatest["getLatest (DB)"]
|
|
16180
|
+
execute --> getLatest["getLatest (DB)<br/>skipped: sharedStore is null"]
|
|
15830
16181
|
execute --> resolveFull["resolveFullConfig()<br/>(merge + validate)"]
|
|
15831
16182
|
|
|
15832
16183
|
resolveLocal --> check["Check restart-required fields"]
|
|
@@ -15845,26 +16196,26 @@ flowchart TD
|
|
|
15845
16196
|
- **Mutex:** Boolean flag prevents concurrent reloads. Second reload returns `{ success: false, errors: ["Reload already in progress"] }`.
|
|
15846
16197
|
- **Debounce:** Rapid triggers (e.g., multiple SIGHUP signals) are collapsed into a single reload with a 500ms window.
|
|
15847
16198
|
- **Validation before swap:** The new config must pass full schema validation. On failure, the old config is preserved and an error is logged.
|
|
15848
|
-
- **Restart-required detection:**
|
|
16199
|
+
- **Restart-required detection:** `databaseUrl`, `port`, `instanceId` and `storage` are compared. If changed, the old values are preserved in the applied config and a warning is logged.
|
|
15849
16200
|
- **No crash on failure:** The orchestrator always keeps running with the old config if anything goes wrong during reload.
|
|
15850
16201
|
|
|
15851
16202
|
### Subsystem callbacks
|
|
15852
16203
|
|
|
15853
16204
|
The `ConfigReloader` uses a dependency injection pattern with callbacks for subsystem re-initialization:
|
|
15854
16205
|
|
|
15855
|
-
| Callback | When Called | Purpose
|
|
15856
|
-
| --------------------- | ----------------------------- |
|
|
15857
|
-
| `onProviderChange` | Provider config changed | Reserved callback
|
|
15858
|
-
| `onScalerReload` | Always on successful reload | Reload scaler YAML config
|
|
15859
|
-
| `onPlatformReconnect` | Platform URL or token changed |
|
|
15860
|
-
| `onConfigApplied` | Always on successful reload | Atomic config reference swap, increment local config version
|
|
16206
|
+
| Callback | When Called | Purpose |
|
|
16207
|
+
| --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
16208
|
+
| `onProviderChange` | Provider config changed | Reserved callback. Providers are DB-managed via the sources table, so the change detector always reports no change |
|
|
16209
|
+
| `onScalerReload` | Always on successful reload | Reload scaler YAML config, from the path the process started with |
|
|
16210
|
+
| `onPlatformReconnect` | Platform URL or token changed | Logs that the Platform connection settings changed. The connection is not re-established; `standalone.ts` registers no handler |
|
|
16211
|
+
| `onConfigApplied` | Always on successful reload | Atomic config reference swap, increment local config version |
|
|
15861
16212
|
|
|
15862
16213
|
### Prometheus metrics
|
|
15863
16214
|
|
|
15864
|
-
| Metric | Type | Labels | Description
|
|
15865
|
-
| ------------------------------- | ------- | ----------------------------------------------------------------------- |
|
|
15866
|
-
| `kici_orch_config_reload_total` | Counter | `result` (attempted/success/failed), `source` (sighup/http/cluster/cli) | Config reload attempts and outcomes
|
|
15867
|
-
| `kici_orch_config_version` | Gauge | -- |
|
|
16215
|
+
| Metric | Type | Labels | Description |
|
|
16216
|
+
| ------------------------------- | ------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
16217
|
+
| `kici_orch_config_reload_total` | Counter | `result` (attempted/success/failed), `source` (sighup/http/cluster/cli) | Config reload attempts and outcomes |
|
|
16218
|
+
| `kici_orch_config_version` | Gauge | -- | Shared config version from the DB. Set only when the reload path reads a version, so it carries no value today |
|
|
15868
16219
|
|
|
15869
16220
|
## Multi-Provider
|
|
15870
16221
|
|
|
@@ -15896,40 +16247,42 @@ Each source record contains its own `appId` and `privateKey` (stored as scoped s
|
|
|
15896
16247
|
|
|
15897
16248
|
### Heartbeat config version
|
|
15898
16249
|
|
|
15899
|
-
In clustered deployments, each orchestrator includes its
|
|
16250
|
+
In clustered deployments, each orchestrator includes its config version in Raft heartbeat metadata via the `configVersion` optional field on the `peerHeartbeatSchema`.
|
|
16251
|
+
|
|
16252
|
+
**That number is a local reload counter, not a shared config version.** `onConfigApplied` increments it on every successful reload and publishes the new value to the peer registry. It counts how many times this instance has reloaded.
|
|
15900
16253
|
|
|
15901
16254
|
When the `PeerRegistry` processes a heartbeat:
|
|
15902
16255
|
|
|
15903
16256
|
1. Compare `localConfigVersion` with `peer.configVersion`
|
|
15904
16257
|
2. If `peer.configVersion > localConfigVersion` AND both are > 0:
|
|
15905
16258
|
- Invoke the `onConfigVersionBehind` callback
|
|
15906
|
-
- This triggers a config reload
|
|
16259
|
+
- This triggers a config reload, which re-reads the environment and the local YAML file
|
|
15907
16260
|
|
|
15908
16261
|
### Auto-remediation flow
|
|
15909
16262
|
|
|
15910
16263
|
```
|
|
15911
|
-
Orchestrator A (
|
|
16264
|
+
Orchestrator A (reloaded 5x) Orchestrator B (reloaded 3x)
|
|
15912
16265
|
│ │
|
|
15913
16266
|
│──── heartbeat(configVersion=5) ────>│
|
|
15914
16267
|
│ │
|
|
15915
16268
|
│ compare: 5 > 3
|
|
15916
|
-
│ trigger reload
|
|
16269
|
+
│ trigger reload
|
|
15917
16270
|
│ │
|
|
15918
|
-
│ resolveFullConfig()
|
|
15919
|
-
│ ->
|
|
16271
|
+
│ resolveFullConfig(local, null)
|
|
16272
|
+
│ -> counter becomes 4
|
|
15920
16273
|
│ │
|
|
15921
|
-
│<── heartbeat(configVersion=
|
|
16274
|
+
│<── heartbeat(configVersion=4) ──────│
|
|
15922
16275
|
│ │
|
|
15923
|
-
│ both at version 5 ✓ │
|
|
15924
16276
|
```
|
|
15925
16277
|
|
|
16278
|
+
B converges on A's count only after it has reloaded as many times as A has. Each instance reads its own environment and its own YAML file, so the two agree on content only when those inputs agree.
|
|
16279
|
+
|
|
15926
16280
|
### Guard conditions
|
|
15927
16281
|
|
|
15928
16282
|
- Version comparison only triggers when **both** local and peer versions are > 0
|
|
15929
16283
|
- This prevents false triggers from:
|
|
15930
|
-
-
|
|
15931
|
-
- Newly started orchestrators before their first
|
|
15932
|
-
- The `localConfigVersion` is a monotonically incrementing local counter (incremented on each successful reload)
|
|
16284
|
+
- Orchestrators that do not report `configVersion` (field is optional, defaults to 0)
|
|
16285
|
+
- Newly started orchestrators before their first reload
|
|
15933
16286
|
|
|
15934
16287
|
## See also
|
|
15935
16288
|
|
|
@@ -15960,7 +16313,7 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
|
|
|
15960
16313
|
|
|
15961
16314
|
1. **Provider sends webhook** to the Platform relay endpoint.
|
|
15962
16315
|
2. **Platform routes the webhook** to the right orchestrator over WebSocket and forwards the body bytes verbatim. Platform never sees customer HMAC secrets — signature verification happens entirely on the orchestrator after reassembly.
|
|
15963
|
-
3. **Orchestrator verifies signature** (HMAC-SHA256 against per-source webhook secret, with dual-secret rotation support).
|
|
16316
|
+
3. **Orchestrator admits the delivery**, then **verifies the signature** (HMAC-SHA256 against per-source webhook secret, with dual-secret rotation support). Admission runs first, on the routing key alone: when the ingest admission controller sheds, the orchestrator records an `event_log` breadcrumb with status `shed` and ACKs `shed_retry_later`, which the Platform answers as **429** with `Retry-After`. See [ingest admission shed](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#ingest-admission-shed-step-3).
|
|
15964
16317
|
4. **Orchestrator dedup check** against dual-layer `DedupCache` (in-memory set + `dedup_cache` DB table).
|
|
15965
16318
|
5. **Orchestrator resolves provider** by looking up the provider bundle from the `ProviderRegistry` using `getByRoutingKey()` (exact match first, falls back to provider type prefix for backward compatibility). Skips processing if the provider is unknown.
|
|
15966
16319
|
6. **Orchestrator normalizes** the webhook via the provider's `WebhookNormalizer` (extracts branch, event type, action, sender).
|
|
@@ -16087,7 +16440,7 @@ Build Job Dispatch --> Build Agent (kici:role:builder + matching kici:os:/kici:a
|
|
|
16087
16440
|
| |-- npm ci in .kici/
|
|
16088
16441
|
| |-- Pack .kici/ source (portable tar.gz, excludes node_modules)
|
|
16089
16442
|
| |-- Pack .kici/node_modules (portable tar.gz)
|
|
16090
|
-
| |-- Upload source tarball to cache (source/{
|
|
16443
|
+
| |-- Upload source tarball to cache (source/v2/{orgId}/{sourceTarDigest}.tar.gz)
|
|
16091
16444
|
| |-- Upload deps tarball to cache (deps/{plat}-{arch}/{depsHash}.tar.gz)
|
|
16092
16445
|
| |-- Upload deps companion .hash file
|
|
16093
16446
|
| |-- Report success (cache.upload.complete × 2)
|
|
@@ -16162,7 +16515,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
|
|
|
16162
16515
|
|
|
16163
16516
|
### Cross-source / no-contentHash workflows
|
|
16164
16517
|
|
|
16165
|
-
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is
|
|
16518
|
+
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 41.
|
|
16166
16519
|
- **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
|
|
16167
16520
|
|
|
16168
16521
|
### Build deduplication
|
|
@@ -16201,7 +16554,7 @@ Both source and dep caches use `S3CacheStorage` as the sole backend. The `CacheS
|
|
|
16201
16554
|
|
|
16202
16555
|
Cache keys reflect that source tarballs and deps have different platform characteristics:
|
|
16203
16556
|
|
|
16204
|
-
- **Source:** `source/{
|
|
16557
|
+
- **Source:** `source/v2/{orgId}/{sourceTarDigest}.tar.gz`, with a `source/v2/{orgId}/{contentHash}.hash` pointer — platform-agnostic, and scoped to the owning organization so two repositories with matching `.kici/` trees never share one object. Raw TypeScript source is identical regardless of CPU architecture, so one entry is shared across all platforms. `contentHash` is the per-workflow hash from the lock file (`SHA-256(COMPILE_SCHEMA_VERSION + ":" + rawSource [+ "\0" + assetDigest])`, where `COMPILE_SCHEMA_VERSION = 7` and line endings are normalized to LF so the hash agrees across platforms).
|
|
16205
16558
|
- **Deps:** `deps/{platform}-{arch}/{depsHash}.tar.gz`, with a
|
|
16206
16559
|
`deps/{platform}-{arch}/{lockfileHash}.hash` pointer holding that hash — the
|
|
16207
16560
|
tarball is addressed by its own content, so two builds sharing a lock file
|
|
@@ -16932,14 +17285,14 @@ Shared business logic used by all three tiers. Single source of truth for cross-
|
|
|
16932
17285
|
- Label utilities (platform label derivation, runsOn normalization, `kici:*` set-only reserved namespace, role labels)
|
|
16933
17286
|
- Host inventory (the canonical queryable host-roster schema shared by the orchestrator's roster store, the agent-facing inventory API, and the SDK's `ctx.kici.inventory`)
|
|
16934
17287
|
- Audit policy and retention (per-action access-log sampling, warm-retention windows for cold-store eligibility, federated activity row schema)
|
|
16935
|
-
- Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`, `event`) and the reserved `kici.` event-name prefix that keeps a user step from forging a system event
|
|
17288
|
+
- Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`, `event`; the orchestrator config rejects `kubernetes`) and the reserved `kici.` event-name prefix that keeps a user step from forging a system event
|
|
16936
17289
|
- Job resource vocabulary (the requests/limits shape the SDK accepts, the compiler validates and emits, the orchestrator uses for capacity math and kernel-side enforcement, and the dashboard displays)
|
|
16937
17290
|
- Registration trigger type enum (registerable trigger discriminator)
|
|
16938
17291
|
- Sandbox capability set (the Linux capability names a container sandbox may add or drop, shared by the SDK validator, the compiler, and the dispatch resolver)
|
|
16939
17292
|
- Plan tier vocabulary (the hosted plan tiers and the purchasable subset, shared by the Platform and the browser dashboard)
|
|
16940
17293
|
- Infrastructure alert vocabulary (the diagnostics alert types and severities the Platform mints and the dashboard and `kici` CLI render)
|
|
16941
17294
|
- Metric catalog (the generated Prometheus metric inventory, its naming policy, and metric-kind compatibility checks)
|
|
16942
|
-
- Bundler config (shared
|
|
17295
|
+
- Bundler config (the shared workflow-bundle configuration factory on the barrel; the agent runtime uses the `@kici-dev/core/ts-loader-hook` to transform TypeScript on import, so no runtime path bundles a workflow)
|
|
16943
17296
|
|
|
16944
17297
|
> Source: `packages/engine/src/`
|
|
16945
17298
|
|
|
@@ -16959,7 +17312,7 @@ It also runs the **local dev plane** -- an on-demand, fully local execution stac
|
|
|
16959
17312
|
|
|
16960
17313
|
### `@kici-dev/core`
|
|
16961
17314
|
|
|
16962
|
-
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). 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.
|
|
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.
|
|
16963
17316
|
|
|
16964
17317
|
> Source: `packages/core/src/`
|
|
16965
17318
|
|