@kici-dev/compiler 0.6.1 → 0.8.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 +2 -2
- 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/local.d.ts +9 -2
- package/dist/commands/local.js +14 -4
- 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 +4 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/runs/logs.js +3 -2
- 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 +73 -87
- package/dist/llm-context/llms-cli-remote.txt +53 -8
- package/dist/llm-context/llms-cli.txt +71 -36
- 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 +558 -180
- package/dist/llm-context/llms-getting-started.txt +5 -5
- 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 +33 -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 +6 -14
- package/dist/local-plane/paths.d.ts +1 -0
- package/dist/local-plane/paths.js +1 -0
- package/dist/local-plane/plane-log.d.ts +27 -0
- package/dist/local-plane/plane-log.js +39 -0
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/local-plane/plane-trigger.d.ts +28 -0
- package/dist/local-plane/plane-trigger.js +57 -2
- package/dist/local-plane/postgres.js +9 -6
- package/dist/local-plane/run-follow.js +2 -1
- package/dist/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/remote/output/streaming.d.ts +12 -0
- package/dist/remote/output/streaming.js +20 -1
- package/dist/remote/platform-client.d.ts +2 -0
- package/dist/templates/package-json.d.ts +9 -7
- package/dist/templates/package-json.js +11 -9
- 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)
|
|
@@ -590,7 +590,7 @@ Point to a local SDK checkout for IDE autocompletion:
|
|
|
590
590
|
{
|
|
591
591
|
"name": "my-project-kici",
|
|
592
592
|
"devDependencies": {
|
|
593
|
-
"@kici-dev/sdk": "
|
|
593
|
+
"@kici-dev/sdk": "latest"
|
|
594
594
|
},
|
|
595
595
|
"kici": {
|
|
596
596
|
"sdkPath": "../../packages/sdk"
|
|
@@ -602,7 +602,7 @@ The `sdkPath` field tells the compiler where to resolve TypeScript path mappings
|
|
|
602
602
|
|
|
603
603
|
### KICI_DEV environment variable
|
|
604
604
|
|
|
605
|
-
Set `KICI_DEV=true` to
|
|
605
|
+
Set `KICI_DEV=true` to pin the SDK to the `latest` dist-tag in generated files, which resolves prerelease builds from a local Verdaccio registry:
|
|
606
606
|
|
|
607
607
|
```bash
|
|
608
608
|
KICI_DEV=true npx kici init
|
|
@@ -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
|
|
|
@@ -6386,6 +6502,17 @@ interface StepContext<TInputs = Record<string, unknown>> {
|
|
|
6386
6502
|
provider?: string;
|
|
6387
6503
|
/** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */
|
|
6388
6504
|
isTestRun: boolean;
|
|
6505
|
+
/** The job's own checked-out repository (present for every job that checks out); `withWrite` opens a write window for it — see [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/) */
|
|
6506
|
+
repo?: {
|
|
6507
|
+
identifier: string;
|
|
6508
|
+
path: string;
|
|
6509
|
+
ref?: string;
|
|
6510
|
+
sha?: string;
|
|
6511
|
+
withWrite(
|
|
6512
|
+
opts: { permissions?: Record<string, string>; credential?: string },
|
|
6513
|
+
fn: () => Promise<void>,
|
|
6514
|
+
): Promise<void>;
|
|
6515
|
+
};
|
|
6389
6516
|
/** Registering repo of a global workflow (undefined for non-global workflows) */
|
|
6390
6517
|
workflowRepo?: RepoInfo;
|
|
6391
6518
|
/** Repo where the triggering event occurred (undefined for non-global workflows) */
|
|
@@ -6586,6 +6713,8 @@ const deploy = job('deploy', {
|
|
|
6586
6713
|
|
|
6587
6714
|
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
6715
|
|
|
6716
|
+
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.
|
|
6717
|
+
|
|
6589
6718
|
```typescript
|
|
6590
6719
|
const publish = job('publish', {
|
|
6591
6720
|
steps: [
|
|
@@ -6625,6 +6754,8 @@ interface HostInventoryEntry {
|
|
|
6625
6754
|
|
|
6626
6755
|
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
6756
|
|
|
6757
|
+
`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.
|
|
6758
|
+
|
|
6628
6759
|
```typescript
|
|
6629
6760
|
// All hosts:
|
|
6630
6761
|
const all = await ctx.kici.inventory.query();
|
|
@@ -7320,7 +7451,7 @@ triggered, or the repo had no lock file at that commit.
|
|
|
7320
7451
|
source-registration mismatch).
|
|
7321
7452
|
2. **Did anything match?** Run `kici preview push --branch <your-branch>`
|
|
7322
7453
|
against your workflow. If it reports no matching workflow, your triggers don't
|
|
7323
|
-
cover that event/branch — the push was delivered and
|
|
7454
|
+
cover that event/branch — the push was delivered and matched nothing.
|
|
7324
7455
|
3. **Was there a lock file?** A repository with **no** `kici.lock.json` at the
|
|
7325
7456
|
pushed commit produces no run and is not an error. Confirm the lock file is
|
|
7326
7457
|
committed and current (see [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift)).
|
|
@@ -7639,7 +7770,7 @@ KiCI uses a **two-artifact model**: TypeScript workflows are the source of truth
|
|
|
7639
7770
|
## Why the lock file matters
|
|
7640
7771
|
|
|
7641
7772
|
- **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
|
|
7773
|
+
- **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
7774
|
|
|
7644
7775
|
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
7776
|
|
|
@@ -7647,37 +7778,38 @@ If you change a workflow file (`.ts`) but do **not** regenerate and commit the l
|
|
|
7647
7778
|
|
|
7648
7779
|
The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
|
|
7649
7780
|
|
|
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
|
-
| `
|
|
7781
|
+
| Field | Description |
|
|
7782
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
7783
|
+
| `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. |
|
|
7784
|
+
| `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). |
|
|
7785
|
+
| `source` | Reference to the source file and export (e.g., `{ file: '.kici/workflows/ci.ts', export: '#default' }`). |
|
|
7786
|
+
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
|
|
7787
|
+
| `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. |
|
|
7788
|
+
| `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. |
|
|
7789
|
+
| `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
|
|
7658
7790
|
|
|
7659
7791
|
Each workflow entry includes:
|
|
7660
7792
|
|
|
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`.
|
|
7793
|
+
| Field | Description |
|
|
7794
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7795
|
+
| `name` | Workflow name. |
|
|
7796
|
+
| `source` | Per-workflow source file and export reference. |
|
|
7797
|
+
| `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. |
|
|
7798
|
+
| `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. |
|
|
7799
|
+
| `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching). |
|
|
7800
|
+
| `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, contexts, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
|
|
7801
|
+
| `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
|
|
7802
|
+
| `description` | Optional workflow description. |
|
|
7803
|
+
| `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). |
|
|
7804
|
+
| `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
|
|
7805
|
+
| `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
|
|
7806
|
+
| `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/). |
|
|
7807
|
+
| `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/). |
|
|
7808
|
+
| `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
|
|
7809
|
+
| `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
|
|
7810
|
+
| `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/). |
|
|
7811
|
+
| `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). |
|
|
7812
|
+
| Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
|
|
7681
7813
|
|
|
7682
7814
|
Step entries carry their own capability flags, so the orchestrator can reason about a step without loading your TypeScript:
|
|
7683
7815
|
|
|
@@ -7758,9 +7890,40 @@ kici compile --check
|
|
|
7758
7890
|
|
|
7759
7891
|
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
7892
|
|
|
7893
|
+
## Files the content hash skips (`.kici/.kiciignore`)
|
|
7894
|
+
|
|
7895
|
+
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:
|
|
7896
|
+
|
|
7897
|
+
```
|
|
7898
|
+
node_modules/
|
|
7899
|
+
types/
|
|
7900
|
+
.npmrc
|
|
7901
|
+
package-lock.json
|
|
7902
|
+
pnpm-lock.yaml
|
|
7903
|
+
kici.lock.json
|
|
7904
|
+
```
|
|
7905
|
+
|
|
7906
|
+
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.
|
|
7907
|
+
|
|
7908
|
+
`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.
|
|
7909
|
+
|
|
7910
|
+
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/`.
|
|
7911
|
+
|
|
7912
|
+
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.
|
|
7913
|
+
|
|
7914
|
+
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`.
|
|
7915
|
+
|
|
7916
|
+
:::caution[The file replaces the defaults — it does not add to them]
|
|
7917
|
+
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.
|
|
7918
|
+
:::
|
|
7919
|
+
|
|
7920
|
+
`.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.
|
|
7921
|
+
|
|
7922
|
+
> **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.
|
|
7923
|
+
|
|
7761
7924
|
## Extra files in the content hash (`hashFiles`)
|
|
7762
7925
|
|
|
7763
|
-
|
|
7926
|
+
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
7927
|
|
|
7765
7928
|
Use the `hashFiles` option on a workflow to include additional paths or glob patterns (relative to the repo root) in the content hash:
|
|
7766
7929
|
|
|
@@ -7771,14 +7934,14 @@ export default workflow('deploy', {
|
|
|
7771
7934
|
});
|
|
7772
7935
|
```
|
|
7773
7936
|
|
|
7774
|
-
When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" +
|
|
7937
|
+
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
7938
|
|
|
7776
7939
|
## Agent-side safety net
|
|
7777
7940
|
|
|
7778
7941
|
If drift still occurs (e.g. someone committed only the `.ts` change), the agent detects it at run time before any step runs:
|
|
7779
7942
|
|
|
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.
|
|
7943
|
+
- 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.
|
|
7944
|
+
- 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
7945
|
|
|
7783
7946
|
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
7947
|
|
|
@@ -7790,6 +7953,7 @@ So even without a pre-commit or CI check, a stale lock file will cause the run t
|
|
|
7790
7953
|
| Catch drift before commit | Install a pre-commit hook with `kici hook install`. |
|
|
7791
7954
|
| Catch broken source in CI | Run `kici compile --check` in CI. |
|
|
7792
7955
|
| Bust cache on external files | Add `hashFiles: ['config.json']` to include non-workflow files in the content hash. |
|
|
7956
|
+
| Skip a path inside `.kici/` | List it in `.kici/.kiciignore` — remember the file replaces the defaults. |
|
|
7793
7957
|
| Fail fast when drift remains | Rely on the agent’s hash verification when it compiles from source. |
|
|
7794
7958
|
|
|
7795
7959
|
## See also
|
|
@@ -8420,7 +8584,7 @@ Manage the **local dev plane** — the warm, per-user orchestrator (plus its own
|
|
|
8420
8584
|
kici local up [--offline | --connected] # Start, or reuse an already-running plane
|
|
8421
8585
|
kici local status [--json] # Port, pid, PostgreSQL backend, attachment mode, readiness
|
|
8422
8586
|
kici local down # Stop the orchestrator and its PostgreSQL, verifying the port is freed
|
|
8423
|
-
kici local logs # Print the
|
|
8587
|
+
kici local logs # Print the plane log paths and rotation policy
|
|
8424
8588
|
kici local attach # Attach to the Platform (hybrid mode)
|
|
8425
8589
|
kici local detach # Return the plane to offline (independent) mode
|
|
8426
8590
|
kici local trust-root <file> # Export the dev-signed trust root for offline verification
|
|
@@ -8437,6 +8601,8 @@ The plane runs in one of two modes:
|
|
|
8437
8601
|
|
|
8438
8602
|
`kici local status` reports a plane whose process is alive but whose readiness probe fails — for example when its PostgreSQL has stopped — as running but not ready, together with its readiness checks, rather than as not running. When the holder is a KiCI plane orchestrator that this config directory did not start — a plane belonging to another `KICI_CONFIG_DIR`, or one whose record here was lost — status names it as such rather than as not ready, since its readiness is never probed, and points at `kici local down`, which does reclaim it. When the port is held by a process that is not a KiCI plane orchestrator, `kici local status` names that holder instead and points at `KICI_LOCAL_ORCH_PORT`, because `kici local down` will not stop it.
|
|
8439
8603
|
|
|
8604
|
+
The plane writes its orchestrator log to `orchestrator.log` in its state directory and, when it runs embedded PostgreSQL, the PostgreSQL log to `orchestrator.log.pg` beside it. `kici local logs` prints their location. Each is rotated to a `.1` sibling when it reaches 50 MB, at the next plane start — the plane keeps the current log and one previous generation, so neither can grow without bound.
|
|
8605
|
+
|
|
8440
8606
|
Pass `--json` for machine-readable output. It prints one object and exits 0 for
|
|
8441
8607
|
every state, including when the plane is stopped — the state is in the payload,
|
|
8442
8608
|
not the exit code:
|
|
@@ -8729,7 +8895,7 @@ Synopsis: `kici local down`
|
|
|
8729
8895
|
|
|
8730
8896
|
### `kici local logs`
|
|
8731
8897
|
|
|
8732
|
-
Print the local dev plane
|
|
8898
|
+
Print the local dev plane log paths and rotation policy
|
|
8733
8899
|
|
|
8734
8900
|
Synopsis: `kici local logs`
|
|
8735
8901
|
|
|
@@ -8959,6 +9125,12 @@ If you belong to a single organization, the org is resolved automatically. If
|
|
|
8959
9125
|
you belong to several, pass an `orgId` argument to any tool (use `list_orgs` to
|
|
8960
9126
|
find it).
|
|
8961
9127
|
|
|
9128
|
+
Only an **active** membership counts: an organization you have been suspended
|
|
9129
|
+
in, one that has been disabled, and one that has been deleted are all skipped.
|
|
9130
|
+
So a single active membership alongside a disabled one still resolves
|
|
9131
|
+
automatically, and a tool call against an organization you are suspended in is
|
|
9132
|
+
refused with the same message the dashboard gives.
|
|
9133
|
+
|
|
8962
9134
|
### Limits and pagination
|
|
8963
9135
|
|
|
8964
9136
|
The MCP server applies a few bounds so an agent loop can't overwhelm the shared
|
|
@@ -9048,7 +9220,7 @@ you hold, so an agent cannot escalate beyond its creator.
|
|
|
9048
9220
|
|
|
9049
9221
|
**Repository scope comes along too.** If your role is restricted to a set of
|
|
9050
9222
|
repositories, an agent token you mint is restricted to the same set. Runs
|
|
9051
|
-
outside it are
|
|
9223
|
+
outside it are not there: they are filtered out of `list_runs`,
|
|
9052
9224
|
`cancel_runs_by_branch` skips them, and naming one directly answers "not found"
|
|
9053
9225
|
— the same answer a run id that does not exist gets, so an agent cannot use the
|
|
9054
9226
|
tools to discover which repositories it is missing. Your organization's audit
|
|
@@ -9291,14 +9463,18 @@ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMid
|
|
|
9291
9463
|
|
|
9292
9464
|
### Configurable surfaces
|
|
9293
9465
|
|
|
9294
|
-
The dashboard is a browser SPA on top of the same `/api/v1/*` surface, so
|
|
9466
|
+
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
9467
|
|
|
9296
|
-
- **Auth & identity:** `/cli/exchange-token
|
|
9297
|
-
- **Org & membership:** `/orgs
|
|
9468
|
+
- **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}`†
|
|
9469
|
+
- **Org & membership:** `/orgs` (POST)†, `/orgs/:customerId`, `/orgs/:customerId/{members,roles,api-keys,orchestrator-keys,service-accounts,billing,trust-policies}`
|
|
9298
9470
|
- **Workflows & runs:** `/orgs/:customerId/{runs,registrations,workflows,held-runs,contexts,secrets,global-workflows}`
|
|
9299
9471
|
- **Webhooks & event log:** `/orgs/:customerId/{sources,webhook-endpoints,event-log}`
|
|
9300
9472
|
- **Diagnostics & activity:** `/orgs/:customerId/{diagnostics,activity,access-log}`
|
|
9301
9473
|
|
|
9474
|
+
**† 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.
|
|
9475
|
+
|
|
9476
|
+
`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.
|
|
9477
|
+
|
|
9302
9478
|
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
9479
|
|
|
9304
9480
|
### Calling the API
|
|
@@ -9317,11 +9493,16 @@ curl -sS \
|
|
|
9317
9493
|
|
|
9318
9494
|
**Browser console (after dashboard login):**
|
|
9319
9495
|
|
|
9496
|
+
Mint a personal access token and pass it explicitly. Do not script against the
|
|
9497
|
+
dashboard's own session token. That token is short-lived and tied to your
|
|
9498
|
+
identity-provider session, so anything built on it stops working at the next
|
|
9499
|
+
renewal or sign-out.
|
|
9500
|
+
|
|
9320
9501
|
```js
|
|
9321
|
-
|
|
9322
|
-
const
|
|
9502
|
+
// kici pat create --name console --expires-in-days 1 → prints the token
|
|
9503
|
+
const token = 'kici_pat_...';
|
|
9323
9504
|
const res = await fetch('/<deployment-slug>/api/v1/orgs/<your-org-id>/runs?limit=5', {
|
|
9324
|
-
headers: { Authorization: `Bearer ${
|
|
9505
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
9325
9506
|
});
|
|
9326
9507
|
console.log(await res.json());
|
|
9327
9508
|
```
|
|
@@ -9346,6 +9527,12 @@ The CLI stores authentication data in `~/.kici/config` with `0600` permissions (
|
|
|
9346
9527
|
- Routing key for webhook source identification
|
|
9347
9528
|
- API key, when you logged in with `--token`
|
|
9348
9529
|
|
|
9530
|
+
The web dashboard holds none of these. It keeps only the tokens of your current
|
|
9531
|
+
sign-in, in browser storage for the tab's origin. It does not request offline
|
|
9532
|
+
access, so those tokens die with your identity-provider session instead of
|
|
9533
|
+
staying valid for weeks. Its origin also serves a Content-Security-Policy that
|
|
9534
|
+
restricts which scripts run and which hosts the page may contact.
|
|
9535
|
+
|
|
9349
9536
|
## Troubleshooting
|
|
9350
9537
|
|
|
9351
9538
|
### Browser doesn't open
|
|
@@ -9550,10 +9737,14 @@ kici init --private-registry https://npm.pkg.github.com/ \
|
|
|
9550
9737
|
types/ # Directory for generated type declarations (kici types)
|
|
9551
9738
|
package.json # Dependencies (@kici-dev/sdk)
|
|
9552
9739
|
tsconfig.json # TypeScript configuration (includes types/**/*.d.ts)
|
|
9740
|
+
.gitignore # Keeps the generated types/ declarations untracked
|
|
9741
|
+
.kiciignore # Paths the workflow content hash does not cover
|
|
9553
9742
|
AGENTS.md # LLM authoring context (skip with --no-agents-md)
|
|
9554
9743
|
.kiciignore # Default exclusion patterns for test uploads
|
|
9555
9744
|
```
|
|
9556
9745
|
|
|
9746
|
+
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.
|
|
9747
|
+
|
|
9557
9748
|
`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
9749
|
|
|
9559
9750
|
In interactive mode (TTY), `kici init` prompts you to:
|
|
@@ -9569,7 +9760,7 @@ In interactive mode (TTY), `kici init` prompts you to:
|
|
|
9569
9760
|
|
|
9570
9761
|
**Standalone vs workspace integration:** by default `kici init` scaffolds a self-contained `.kici/` with its own `package.json`. When run inside a pnpm, npm, or yarn workspace, it offers an **integrate** option (or pass `--workspace`): `.kici/` joins the workspace, `@kici-dev/sdk` is added to your workspace-root `package.json`, and your workflows can `import` your other workspace packages (e.g. shared build or deploy utilities). In this mode there is no `.kici/package.json` — the workspace root manages dependencies, and the root install resolves the SDK. Your workflows resolve sibling packages through the workspace-root `node_modules`: under npm and yarn every workspace member is hoisted there automatically, while pnpm links only your root's declared dependencies, so under pnpm add the package you want to import to your workspace-root `dependencies` (this is how KiCI's own repository imports its packages from workflows). Pass `--standalone` to force the self-contained layout even inside a workspace. In CI / non-interactive runs the default is standalone; use `--workspace` to opt in explicitly. `--workspace` and `--standalone` are mutually exclusive, and `--workspace` errors if no workspace is found at or above the current directory.
|
|
9571
9762
|
|
|
9572
|
-
**Development mode:** When `KICI_DEV=true` or `package.json` has `"kici": { "development": true }`, the generated `package.json`
|
|
9763
|
+
**Development mode:** When `KICI_DEV=true` or `package.json` has `"kici": { "development": true }`, the generated `package.json` pins `@kici-dev/sdk` to the `latest` dist-tag so npm resolves Verdaccio's newest prerelease build.
|
|
9573
9764
|
|
|
9574
9765
|
### kici org
|
|
9575
9766
|
|
|
@@ -10094,6 +10285,14 @@ to see it. The bundle holds your CLI, Node and orchestrator versions, your
|
|
|
10094
10285
|
redacted configuration, and your project's workflow and lock-file state. With
|
|
10095
10286
|
`--run` it also holds the failing run's detail and logs.
|
|
10096
10287
|
|
|
10288
|
+
For every orchestrator the probe returned the bundle also records where that
|
|
10289
|
+
orchestrator's own config files live — the paths only, never the contents. An
|
|
10290
|
+
orchestrator that is offline is still listed, with no paths: the CLI reads them
|
|
10291
|
+
from the live connection, so a disconnected one has none to report.
|
|
10292
|
+
Each path is a host path as that orchestrator sees it. A container deployment
|
|
10293
|
+
names a file on the container host, which is not a file you can open from the
|
|
10294
|
+
machine that read the bundle.
|
|
10295
|
+
|
|
10097
10296
|
```bash
|
|
10098
10297
|
kici report [options]
|
|
10099
10298
|
```
|
|
@@ -10507,6 +10706,18 @@ Secrets are always sourced from your real `.kici/` directory, not from the isola
|
|
|
10507
10706
|
|
|
10508
10707
|
Pass `--in-place` to run against the real working directory instead — useful when you explicitly want in-tree execution. `--in-place` requires no git repository; the default isolated mode does, and fails with an actionable error pointing at `--in-place` when the directory is not a git repository.
|
|
10509
10708
|
|
|
10709
|
+
**If the trigger times out:**
|
|
10710
|
+
|
|
10711
|
+
The command waits up to 60 seconds for the local dev plane to create the run. If nothing appears in that window it stops and names the cause it read back from the plane:
|
|
10712
|
+
|
|
10713
|
+
| Message | What it means |
|
|
10714
|
+
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10715
|
+
| `plane is not leader yet (election grace period)` | The plane had not finished electing itself. A single-machine plane elects within seconds; if it does not, read the plane log (`kici local logs`). |
|
|
10716
|
+
| `no kici.lock.json at <commit> in <path>` | The plane resolved no lock file for the commit the run packed, so nothing matched. Commit `.kici/kici.lock.json`, or make sure it is present in the working tree. |
|
|
10717
|
+
| `the plane recorded delivery <id> as "<status>"` | The plane processed the trigger and wrote down a terminal status for it without creating a run. The status names the stage that stopped. |
|
|
10718
|
+
| `no run appeared for delivery <id> — see <log>` | Neither the plane's cluster state nor its delivery record explained the timeout. The named log is the next place to look. |
|
|
10719
|
+
| `no run appeared for this trigger — the plane never accepted the webhook` | Every webhook POST came back without a delivery id, so the plane never queued the trigger. Check the plane is up (`kici local status`), then read the named log. |
|
|
10720
|
+
|
|
10510
10721
|
**Examples:**
|
|
10511
10722
|
|
|
10512
10723
|
```bash
|
|
@@ -11245,7 +11456,7 @@ You declare a gate in your workflow with `approval`. It is available at three le
|
|
|
11245
11456
|
- **Job** — hold the job before any of its steps run.
|
|
11246
11457
|
- **Workflow** — hold the whole run before any job is dispatched.
|
|
11247
11458
|
|
|
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-
|
|
11459
|
+
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
11460
|
|
|
11250
11461
|
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
11462
|
|
|
@@ -11316,7 +11527,7 @@ approval: {
|
|
|
11316
11527
|
|
|
11317
11528
|
| Field | Type | Description |
|
|
11318
11529
|
| ----------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
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-
|
|
11530
|
+
| `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
11531
|
| `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
|
|
11321
11532
|
| `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
|
|
11322
11533
|
| `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 +11706,7 @@ Available rules:
|
|
|
11495
11706
|
- **Branch restrictions** — only allow specific branches to deploy.
|
|
11496
11707
|
- **Required reviewer approvals** — gate the run on human sign-off.
|
|
11497
11708
|
- **Wait timers** — delay execution for a fixed period.
|
|
11498
|
-
- **Concurrency limits** —
|
|
11709
|
+
- **Concurrency limits** — cap how many jobs run against the context at once.
|
|
11499
11710
|
|
|
11500
11711
|
<!-- /help:contexts-protection -->
|
|
11501
11712
|
|
|
@@ -11584,7 +11795,7 @@ job('deploy', {
|
|
|
11584
11795
|
|
|
11585
11796
|
**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
11797
|
|
|
11587
|
-
**Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context)
|
|
11798
|
+
**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
11799
|
|
|
11589
11800
|
**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
11801
|
|
|
@@ -11787,6 +11998,36 @@ The children of a matrix job count individually against the limit. A three-child
|
|
|
11787
11998
|
matrix bound to a context with a limit of two dispatches two children and applies
|
|
11788
11999
|
the strategy above to the third.
|
|
11789
12000
|
|
|
12001
|
+
A job counts against the limit from the moment it is dispatched until it
|
|
12002
|
+
finishes. It does not have to reach an agent first.
|
|
12003
|
+
|
|
12004
|
+
#### What the limit does and does not guarantee
|
|
12005
|
+
|
|
12006
|
+
A context concurrency limit is a **throughput control**. Treat it as a cap on
|
|
12007
|
+
how much work runs at once, not as a lock.
|
|
12008
|
+
|
|
12009
|
+
Two events that arrive in the same instant read the limit before either job is
|
|
12010
|
+
recorded, so each can be admitted. The window is short — the time between one
|
|
12011
|
+
read and one write — but it is real, and it grows with the number of
|
|
12012
|
+
orchestrator processes serving the context.
|
|
12013
|
+
|
|
12014
|
+
When a job must never run beside another copy of itself — a production deploy,
|
|
12015
|
+
a database migration — declare a workflow-level
|
|
12016
|
+
[concurrency group](https://docs.kici.dev/user/concurrency/) as well:
|
|
12017
|
+
|
|
12018
|
+
```typescript
|
|
12019
|
+
export default workflow('deploy', {
|
|
12020
|
+
on: push({ branches: ['main'] }),
|
|
12021
|
+
concurrency: { group: () => 'deploy-prod', max: 1 },
|
|
12022
|
+
jobs: [/* ... */],
|
|
12023
|
+
});
|
|
12024
|
+
```
|
|
12025
|
+
|
|
12026
|
+
That mechanism claims its slot inside a single database transaction, so two
|
|
12027
|
+
runs arriving together cannot both take it. The two are complementary: the
|
|
12028
|
+
context limit caps throughput across every workflow bound to the context, and
|
|
12029
|
+
the concurrency group serializes one workflow against itself.
|
|
12030
|
+
|
|
11790
12031
|
## Dashboard management
|
|
11791
12032
|
|
|
11792
12033
|
### Creating contexts
|
|
@@ -12093,7 +12334,7 @@ registries: [
|
|
|
12093
12334
|
## Security model
|
|
12094
12335
|
|
|
12095
12336
|
- **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
|
|
12337
|
+
- **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
12338
|
- **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
12339
|
- **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
|
|
12099
12340
|
- **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 +12370,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
|
|
|
12129
12370
|
## See also
|
|
12130
12371
|
|
|
12131
12372
|
- [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 (
|
|
12373
|
+
- [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (branch restrictions, required reviewers, minimum trust) that the install gate inherits.
|
|
12133
12374
|
- [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
12375
|
|
|
12135
12376
|
---
|
|
@@ -12247,6 +12488,107 @@ and the step never holds signing credentials — the orchestrator mints and sign
|
|
|
12247
12488
|
the token on the step's behalf from its own run records. Like `attestProvenance`,
|
|
12248
12489
|
it is only available inside a running job step.
|
|
12249
12490
|
|
|
12491
|
+
## ID-token claims and cloud trust policies
|
|
12492
|
+
|
|
12493
|
+
A cloud provider's OIDC trust policy decides which builds may assume a role. The
|
|
12494
|
+
token below is what your policy matches on, so read this section before you
|
|
12495
|
+
write one.
|
|
12496
|
+
|
|
12497
|
+
### The claim set
|
|
12498
|
+
|
|
12499
|
+
| Claim | Value |
|
|
12500
|
+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
12501
|
+
| `iss` | Your orchestrator's provenance issuer |
|
|
12502
|
+
| `aud` | The audience you asked for |
|
|
12503
|
+
| `sub` | The build identity — see the two shapes below |
|
|
12504
|
+
| `repository` | `owner/repo` the run acted on |
|
|
12505
|
+
| `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
|
|
12506
|
+
| `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
|
|
12507
|
+
| `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
|
|
12508
|
+
| `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
|
|
12509
|
+
| `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
|
|
12510
|
+
| `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
|
|
12511
|
+
| `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
|
|
12512
|
+
| `actor` | Provider login of the triggering actor |
|
|
12513
|
+
| `sha` | The run's commit |
|
|
12514
|
+
| `workflow_ref` | `<workflow name>@<sha>` |
|
|
12515
|
+
| `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
|
|
12516
|
+
| `org_id` | Your organization id |
|
|
12517
|
+
|
|
12518
|
+
Every claim in the table is **always present**. A value the run did not resolve
|
|
12519
|
+
is `''` or `'unresolved'`, never omitted and never guessed. That matters: an
|
|
12520
|
+
absent claim makes a `StringEquals` condition pass, which would silently remove
|
|
12521
|
+
a constraint you wrote expecting it to be enforced.
|
|
12522
|
+
|
|
12523
|
+
### The two `sub` shapes
|
|
12524
|
+
|
|
12525
|
+
```
|
|
12526
|
+
push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
|
|
12527
|
+
pull request, review repo:<owner/repo>:pull_request
|
|
12528
|
+
```
|
|
12529
|
+
|
|
12530
|
+
The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
|
|
12531
|
+
pull request's `ref` is its base branch. So a ref-bearing subject would be
|
|
12532
|
+
identical for a fork pull request targeting `main` and a trusted push to `main`.
|
|
12533
|
+
A policy pinning that subject would hand your cloud role to any contributor who
|
|
12534
|
+
opened a pull request running the same workflow.
|
|
12535
|
+
|
|
12536
|
+
**A re-run keeps the shape of the run it repeats.** Re-running a pull-request
|
|
12537
|
+
run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
|
|
12538
|
+
commit from the same source. Its `event_name` claim still reads `rerun` — that
|
|
12539
|
+
claim says what started the run, while `sub` says which identity the run
|
|
12540
|
+
presents. A policy that pins the branch-shaped subject therefore does not match
|
|
12541
|
+
a re-run of a pull request, which is the same protection the first run gets.
|
|
12542
|
+
|
|
12543
|
+
### A worked AWS trust policy
|
|
12544
|
+
|
|
12545
|
+
Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
|
|
12546
|
+
ran; it does not tell you whose code ran.
|
|
12547
|
+
|
|
12548
|
+
```json
|
|
12549
|
+
{
|
|
12550
|
+
"Version": "2012-10-17",
|
|
12551
|
+
"Statement": [
|
|
12552
|
+
{
|
|
12553
|
+
"Effect": "Allow",
|
|
12554
|
+
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
|
|
12555
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
12556
|
+
"Condition": {
|
|
12557
|
+
"StringEquals": {
|
|
12558
|
+
"orch.example.com:aud": "sts.amazonaws.com",
|
|
12559
|
+
"orch.example.com:sub": "repo:acme/app:ref:main:workflow:deploy",
|
|
12560
|
+
"orch.example.com:is_fork": "false",
|
|
12561
|
+
"orch.example.com:head_repository": "acme/app",
|
|
12562
|
+
"orch.example.com:trust_tier": "trusted"
|
|
12563
|
+
}
|
|
12564
|
+
}
|
|
12565
|
+
}
|
|
12566
|
+
]
|
|
12567
|
+
}
|
|
12568
|
+
```
|
|
12569
|
+
|
|
12570
|
+
This grants the role only to a run on `main` in `acme/app`, from code in that
|
|
12571
|
+
same repository, triggered by an actor your orchestrator resolved as trusted.
|
|
12572
|
+
A fork pull request fails on all three of the extra conditions, and a run whose
|
|
12573
|
+
context did not resolve fails too — `'unresolved'` matches none of them, so the
|
|
12574
|
+
policy fails closed.
|
|
12575
|
+
|
|
12576
|
+
To let a same-repo pull request assume the role, add a second statement pinning
|
|
12577
|
+
`"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
|
|
12578
|
+
`"head_repository": "acme/app"`.
|
|
12579
|
+
|
|
12580
|
+
### Migrating an existing policy
|
|
12581
|
+
|
|
12582
|
+
If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
|
|
12583
|
+
matching once you upgrade — which is the fix, because it was matching runs it
|
|
12584
|
+
should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
|
|
12585
|
+
conditions above. The same move covers a re-run of a pull request, which
|
|
12586
|
+
presents the pull-request subject too.
|
|
12587
|
+
|
|
12588
|
+
While you migrate, `KICI_OIDC_LEGACY_PR_SUB=1` on the orchestrator restores the
|
|
12589
|
+
old subject. It restores the collision with it, so treat it as a short bridge,
|
|
12590
|
+
not a setting. See [deprecations](https://docs.kici.dev/user/deprecations/).
|
|
12591
|
+
|
|
12250
12592
|
## Verifying an attestation
|
|
12251
12593
|
|
|
12252
12594
|
Verify a bundle with the `kici verify-attestation` command. It establishes the
|
|
@@ -12872,6 +13214,8 @@ workflow('test', {
|
|
|
12872
13214
|
|
|
12873
13215
|
When `max: 1` (default), runs are fully serialized within the group.
|
|
12874
13216
|
|
|
13217
|
+
`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.
|
|
13218
|
+
|
|
12875
13219
|
## Group key examples
|
|
12876
13220
|
|
|
12877
13221
|
### Deploy per environment
|
|
@@ -12931,6 +13275,14 @@ When a workflow has both `concurrency` and `context` protection rules:
|
|
|
12931
13275
|
|
|
12932
13276
|
This means a run that passed approval won't need re-approval if it gets queued by concurrency.
|
|
12933
13277
|
|
|
13278
|
+
The two caps also differ in strength. A workflow-level concurrency group claims
|
|
13279
|
+
its slot inside a single database transaction, so two runs that arrive together
|
|
13280
|
+
cannot both take it. A context
|
|
13281
|
+
[concurrency limit](https://docs.kici.dev/user/contexts/#concurrency-limits) is a throughput control:
|
|
13282
|
+
simultaneous arrivals can each be admitted before either is recorded. Declare a
|
|
13283
|
+
concurrency group for anything that must never run beside itself, whatever the
|
|
13284
|
+
context limit says.
|
|
13285
|
+
|
|
12934
13286
|
## Cancelling queued runs
|
|
12935
13287
|
|
|
12936
13288
|
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 +13493,17 @@ Store the secrets first with `kici-admin secret set`. Pasting a token straight
|
|
|
13141
13493
|
into the workflow is rejected when the workflow is defined, because a token
|
|
13142
13494
|
written into `.kici/` would be committed to your repository.
|
|
13143
13495
|
|
|
13496
|
+
The named context's protection rules run before the secret is read, exactly as
|
|
13497
|
+
they do for [git credentials](https://docs.kici.dev/user/patterns/git-credentials/#what-a-job-may-ask-for).
|
|
13498
|
+
A `prod:` reference from a branch the `prod` context restricts is refused, and
|
|
13499
|
+
the job is dispatched with no registry credentials — so a private image fails to
|
|
13500
|
+
pull rather than being pulled from a branch the context does not allow. The rule
|
|
13501
|
+
that refused it is named in your orchestrator's log, not in the run.
|
|
13502
|
+
|
|
13503
|
+
**An untrusted ref receives no registry credentials.** A fork pull request is
|
|
13504
|
+
dispatched without them, so a private base image fails to pull and a public one
|
|
13505
|
+
is unaffected. The run's reduced-privilege note says so.
|
|
13506
|
+
|
|
13144
13507
|
The username is not a secret, so you may write it directly:
|
|
13145
13508
|
|
|
13146
13509
|
```typescript
|
|
@@ -13150,6 +13513,30 @@ auth: { username: 'ci-bot', tokenSecret: 'prod:REGISTRY_TOKEN' }
|
|
|
13150
13513
|
Your orchestrator resolves these names at dispatch and sends only the resolved
|
|
13151
13514
|
credentials to the agent. The agent never reads your secret store.
|
|
13152
13515
|
|
|
13516
|
+
### Naming the registry
|
|
13517
|
+
|
|
13518
|
+
`auth` also takes a `registry` — the registry host the credentials belong to,
|
|
13519
|
+
such as `reg.internal:5000`.
|
|
13520
|
+
|
|
13521
|
+
With `image` it is optional, because KiCI reads the host off the image
|
|
13522
|
+
reference. With `dockerfile` it is **required**: the base image is named inside
|
|
13523
|
+
your Dockerfile, so there is nothing to read it from. A `dockerfile` job whose
|
|
13524
|
+
`auth` omits `registry` is refused when you define the workflow.
|
|
13525
|
+
|
|
13526
|
+
```typescript
|
|
13527
|
+
container: {
|
|
13528
|
+
dockerfile: '.kici/ci.Dockerfile',
|
|
13529
|
+
auth: {
|
|
13530
|
+
registry: 'reg.internal:5000',
|
|
13531
|
+
usernameSecret: 'prod:REGISTRY_USER',
|
|
13532
|
+
tokenSecret: 'prod:REGISTRY_TOKEN',
|
|
13533
|
+
},
|
|
13534
|
+
},
|
|
13535
|
+
```
|
|
13536
|
+
|
|
13537
|
+
With `dockerfile`, these credentials pull the Dockerfile's own `FROM` base — not
|
|
13538
|
+
a job image, since the job image is the one KiCI builds.
|
|
13539
|
+
|
|
13153
13540
|
### Credentials that only exist at run time
|
|
13154
13541
|
|
|
13155
13542
|
A token fetched during the run — from a cloud registry's login command, for
|
|
@@ -13300,10 +13687,11 @@ The KiCI CLI reads the following environment variables to customize its behavior
|
|
|
13300
13687
|
|
|
13301
13688
|
## Development
|
|
13302
13689
|
|
|
13303
|
-
| Variable
|
|
13304
|
-
|
|
|
13305
|
-
| `KICI_DEV`
|
|
13306
|
-
| `
|
|
13690
|
+
| Variable | Description | Default |
|
|
13691
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
13692
|
+
| `KICI_DEV` | Enable development mode. When `true`, pins `@kici-dev/sdk` to the `latest` dist-tag (so a dev registry's prerelease build resolves) and skips npm version resolution. | unset |
|
|
13693
|
+
| `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 |
|
|
13694
|
+
| `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
13695
|
|
|
13308
13696
|
## Local dev plane
|
|
13309
13697
|
|
|
@@ -13637,7 +14025,7 @@ job('provision', {
|
|
|
13637
14025
|
|
|
13638
14026
|
### Trust tiers on internal triggers
|
|
13639
14027
|
|
|
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.
|
|
14028
|
+
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
14029
|
|
|
13642
14030
|
Four rules resolve the tier, and KiCI applies them in this order:
|
|
13643
14031
|
|
|
@@ -14164,7 +14552,7 @@ The registration log line names the organization it decided against, so a refusa
|
|
|
14164
14552
|
|
|
14165
14553
|
### Secrets are not available
|
|
14166
14554
|
|
|
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
|
|
14555
|
+
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
14556
|
|
|
14169
14557
|
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
14558
|
|
|
@@ -15394,10 +15782,14 @@ kici-admin secret set <orgId> "__source__/<sourceId>" pat --value "<your-forgejo
|
|
|
15394
15782
|
Finally, configure the forge to deliver webhooks to:
|
|
15395
15783
|
|
|
15396
15784
|
```
|
|
15397
|
-
https://<platform-host>/webhook/<orgId>/generic/<
|
|
15785
|
+
https://<platform-host>/webhook/<orgId>/generic/<sourceId>
|
|
15398
15786
|
```
|
|
15399
15787
|
|
|
15400
|
-
with the same secret you passed to `--secret`.
|
|
15788
|
+
with the same secret you passed to `--secret`. `<orgId>` is your Platform
|
|
15789
|
+
organization id — the same value you passed to `--org`, which the source's
|
|
15790
|
+
routing key embeds — and `<sourceId>` is the id `source add` printed. The
|
|
15791
|
+
orchestrator's own ingest URL has the same shape but takes the source **name**
|
|
15792
|
+
in place of the id.
|
|
15401
15793
|
|
|
15402
15794
|
## SSH deploy key
|
|
15403
15795
|
|
|
@@ -15591,7 +15983,7 @@ interface LocalConfig {
|
|
|
15591
15983
|
|
|
15592
15984
|
### SharedConfig
|
|
15593
15985
|
|
|
15594
|
-
|
|
15986
|
+
Settings stored in the PostgreSQL `config_versions` table, written and read by the `/admin/config` routes and their `kici-admin config` commands:
|
|
15595
15987
|
|
|
15596
15988
|
```typescript
|
|
15597
15989
|
interface SharedConfig {
|
|
@@ -15635,11 +16027,13 @@ interface AppConfig {
|
|
|
15635
16027
|
|
|
15636
16028
|
### How they merge
|
|
15637
16029
|
|
|
16030
|
+
`resolveFullConfig()` takes a `LocalConfig` and an optional `SharedConfig` and merges them:
|
|
16031
|
+
|
|
15638
16032
|
```
|
|
15639
16033
|
defaults (getDefaults())
|
|
15640
16034
|
|
|
|
15641
16035
|
v
|
|
15642
|
-
SharedConfig (
|
|
16036
|
+
SharedConfig (argument) ──deepMerge──> merged layer 1+2
|
|
15643
16037
|
|
|
|
15644
16038
|
v
|
|
15645
16039
|
LocalConfig (from YAML) ──deepMerge──> merged layer 1+2+3
|
|
@@ -15656,29 +16050,48 @@ appConfigSchema.safeParse() ──validate──> typed AppConfig
|
|
|
15656
16050
|
|
|
15657
16051
|
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
16052
|
|
|
16053
|
+
**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.
|
|
16054
|
+
|
|
15659
16055
|
## Resolution chain
|
|
15660
16056
|
|
|
15661
|
-
###
|
|
16057
|
+
### Startup
|
|
16058
|
+
|
|
16059
|
+
`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
16060
|
|
|
15663
16061
|
```
|
|
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
|
|
16062
|
+
Process start
|
|
16063
|
+
|
|
|
16064
|
+
v
|
|
16065
|
+
loadConfig() -> envDef.parse(process.env) -> AppConfig
|
|
16066
|
+
|
|
|
16067
|
+
v
|
|
16068
|
+
Connect to PostgreSQL, run migrations
|
|
16069
|
+
|
|
|
16070
|
+
v
|
|
16071
|
+
Start server (HTTP, WS, scaler, cluster)
|
|
15679
16072
|
```
|
|
15680
16073
|
|
|
15681
|
-
|
|
16074
|
+
The database URL therefore has to be an environment variable: the orchestrator needs it to reach PostgreSQL, and the shared config lives in PostgreSQL.
|
|
16075
|
+
|
|
16076
|
+
### Reload
|
|
16077
|
+
|
|
16078
|
+
`resolveLocalConfig()` and `resolveFullConfig()` run on the reload path, not at startup:
|
|
16079
|
+
|
|
16080
|
+
```
|
|
16081
|
+
SIGHUP / POST /admin/config/reload / kici-admin config reload
|
|
16082
|
+
|
|
|
16083
|
+
v
|
|
16084
|
+
resolveLocalConfig() -> YAML file + KICI_ env overlay
|
|
16085
|
+
|
|
|
16086
|
+
v
|
|
16087
|
+
resolveFullConfig(local, null) -> defaults -> YAML -> env -> AppConfig
|
|
16088
|
+
|
|
|
16089
|
+
v
|
|
16090
|
+
Hold databaseUrl, port, instanceId and storage at their startup values
|
|
16091
|
+
|
|
|
16092
|
+
v
|
|
16093
|
+
Atomic swap into ConfigReloader.currentConfig
|
|
16094
|
+
```
|
|
15682
16095
|
|
|
15683
16096
|
### Env var processing
|
|
15684
16097
|
|
|
@@ -15690,43 +16103,6 @@ Environment variables are processed in two stages:
|
|
|
15690
16103
|
|
|
15691
16104
|
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
16105
|
|
|
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
16106
|
## DB schema
|
|
15731
16107
|
|
|
15732
16108
|
### config_versions table
|
|
@@ -15826,7 +16202,7 @@ flowchart TD
|
|
|
15826
16202
|
trigger --> execute["executeReload()<br/>boolean mutex"]
|
|
15827
16203
|
|
|
15828
16204
|
execute --> resolveLocal["resolveLocalConfig()"]
|
|
15829
|
-
execute --> getLatest["getLatest (DB)"]
|
|
16205
|
+
execute --> getLatest["getLatest (DB)<br/>skipped: sharedStore is null"]
|
|
15830
16206
|
execute --> resolveFull["resolveFullConfig()<br/>(merge + validate)"]
|
|
15831
16207
|
|
|
15832
16208
|
resolveLocal --> check["Check restart-required fields"]
|
|
@@ -15845,26 +16221,26 @@ flowchart TD
|
|
|
15845
16221
|
- **Mutex:** Boolean flag prevents concurrent reloads. Second reload returns `{ success: false, errors: ["Reload already in progress"] }`.
|
|
15846
16222
|
- **Debounce:** Rapid triggers (e.g., multiple SIGHUP signals) are collapsed into a single reload with a 500ms window.
|
|
15847
16223
|
- **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:**
|
|
16224
|
+
- **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
16225
|
- **No crash on failure:** The orchestrator always keeps running with the old config if anything goes wrong during reload.
|
|
15850
16226
|
|
|
15851
16227
|
### Subsystem callbacks
|
|
15852
16228
|
|
|
15853
16229
|
The `ConfigReloader` uses a dependency injection pattern with callbacks for subsystem re-initialization:
|
|
15854
16230
|
|
|
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
|
|
16231
|
+
| Callback | When Called | Purpose |
|
|
16232
|
+
| --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
16233
|
+
| `onProviderChange` | Provider config changed | Reserved callback. Providers are DB-managed via the sources table, so the change detector always reports no change |
|
|
16234
|
+
| `onScalerReload` | Always on successful reload | Reload scaler YAML config, from the path the process started with |
|
|
16235
|
+
| `onPlatformReconnect` | Platform URL or token changed | Logs that the Platform connection settings changed. The connection is not re-established; `standalone.ts` registers no handler |
|
|
16236
|
+
| `onConfigApplied` | Always on successful reload | Atomic config reference swap, increment local config version |
|
|
15861
16237
|
|
|
15862
16238
|
### Prometheus metrics
|
|
15863
16239
|
|
|
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 | -- |
|
|
16240
|
+
| Metric | Type | Labels | Description |
|
|
16241
|
+
| ------------------------------- | ------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
16242
|
+
| `kici_orch_config_reload_total` | Counter | `result` (attempted/success/failed), `source` (sighup/http/cluster/cli) | Config reload attempts and outcomes |
|
|
16243
|
+
| `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
16244
|
|
|
15869
16245
|
## Multi-Provider
|
|
15870
16246
|
|
|
@@ -15896,40 +16272,42 @@ Each source record contains its own `appId` and `privateKey` (stored as scoped s
|
|
|
15896
16272
|
|
|
15897
16273
|
### Heartbeat config version
|
|
15898
16274
|
|
|
15899
|
-
In clustered deployments, each orchestrator includes its
|
|
16275
|
+
In clustered deployments, each orchestrator includes its config version in Raft heartbeat metadata via the `configVersion` optional field on the `peerHeartbeatSchema`.
|
|
16276
|
+
|
|
16277
|
+
**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
16278
|
|
|
15901
16279
|
When the `PeerRegistry` processes a heartbeat:
|
|
15902
16280
|
|
|
15903
16281
|
1. Compare `localConfigVersion` with `peer.configVersion`
|
|
15904
16282
|
2. If `peer.configVersion > localConfigVersion` AND both are > 0:
|
|
15905
16283
|
- Invoke the `onConfigVersionBehind` callback
|
|
15906
|
-
- This triggers a config reload
|
|
16284
|
+
- This triggers a config reload, which re-reads the environment and the local YAML file
|
|
15907
16285
|
|
|
15908
16286
|
### Auto-remediation flow
|
|
15909
16287
|
|
|
15910
16288
|
```
|
|
15911
|
-
Orchestrator A (
|
|
16289
|
+
Orchestrator A (reloaded 5x) Orchestrator B (reloaded 3x)
|
|
15912
16290
|
│ │
|
|
15913
16291
|
│──── heartbeat(configVersion=5) ────>│
|
|
15914
16292
|
│ │
|
|
15915
16293
|
│ compare: 5 > 3
|
|
15916
|
-
│ trigger reload
|
|
16294
|
+
│ trigger reload
|
|
15917
16295
|
│ │
|
|
15918
|
-
│ resolveFullConfig()
|
|
15919
|
-
│ ->
|
|
16296
|
+
│ resolveFullConfig(local, null)
|
|
16297
|
+
│ -> counter becomes 4
|
|
15920
16298
|
│ │
|
|
15921
|
-
│<── heartbeat(configVersion=
|
|
16299
|
+
│<── heartbeat(configVersion=4) ──────│
|
|
15922
16300
|
│ │
|
|
15923
|
-
│ both at version 5 ✓ │
|
|
15924
16301
|
```
|
|
15925
16302
|
|
|
16303
|
+
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.
|
|
16304
|
+
|
|
15926
16305
|
### Guard conditions
|
|
15927
16306
|
|
|
15928
16307
|
- Version comparison only triggers when **both** local and peer versions are > 0
|
|
15929
16308
|
- 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)
|
|
16309
|
+
- Orchestrators that do not report `configVersion` (field is optional, defaults to 0)
|
|
16310
|
+
- Newly started orchestrators before their first reload
|
|
15933
16311
|
|
|
15934
16312
|
## See also
|
|
15935
16313
|
|
|
@@ -15960,7 +16338,7 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
|
|
|
15960
16338
|
|
|
15961
16339
|
1. **Provider sends webhook** to the Platform relay endpoint.
|
|
15962
16340
|
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).
|
|
16341
|
+
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
16342
|
4. **Orchestrator dedup check** against dual-layer `DedupCache` (in-memory set + `dedup_cache` DB table).
|
|
15965
16343
|
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
16344
|
6. **Orchestrator normalizes** the webhook via the provider's `WebhookNormalizer` (extracts branch, event type, action, sender).
|
|
@@ -16087,7 +16465,7 @@ Build Job Dispatch --> Build Agent (kici:role:builder + matching kici:os:/kici:a
|
|
|
16087
16465
|
| |-- npm ci in .kici/
|
|
16088
16466
|
| |-- Pack .kici/ source (portable tar.gz, excludes node_modules)
|
|
16089
16467
|
| |-- Pack .kici/node_modules (portable tar.gz)
|
|
16090
|
-
| |-- Upload source tarball to cache (source/{
|
|
16468
|
+
| |-- Upload source tarball to cache (source/v2/{orgId}/{sourceTarDigest}.tar.gz)
|
|
16091
16469
|
| |-- Upload deps tarball to cache (deps/{plat}-{arch}/{depsHash}.tar.gz)
|
|
16092
16470
|
| |-- Upload deps companion .hash file
|
|
16093
16471
|
| |-- Report success (cache.upload.complete × 2)
|
|
@@ -16162,7 +16540,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
|
|
|
16162
16540
|
|
|
16163
16541
|
### Cross-source / no-contentHash workflows
|
|
16164
16542
|
|
|
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
|
|
16543
|
+
- **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
16544
|
- **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
16545
|
|
|
16168
16546
|
### Build deduplication
|
|
@@ -16201,7 +16579,7 @@ Both source and dep caches use `S3CacheStorage` as the sole backend. The `CacheS
|
|
|
16201
16579
|
|
|
16202
16580
|
Cache keys reflect that source tarballs and deps have different platform characteristics:
|
|
16203
16581
|
|
|
16204
|
-
- **Source:** `source/{
|
|
16582
|
+
- **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
16583
|
- **Deps:** `deps/{platform}-{arch}/{depsHash}.tar.gz`, with a
|
|
16206
16584
|
`deps/{platform}-{arch}/{lockfileHash}.hash` pointer holding that hash — the
|
|
16207
16585
|
tarball is addressed by its own content, so two builds sharing a lock file
|
|
@@ -16932,14 +17310,14 @@ Shared business logic used by all three tiers. Single source of truth for cross-
|
|
|
16932
17310
|
- Label utilities (platform label derivation, runsOn normalization, `kici:*` set-only reserved namespace, role labels)
|
|
16933
17311
|
- 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
17312
|
- 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
|
|
17313
|
+
- 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
17314
|
- 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
17315
|
- Registration trigger type enum (registerable trigger discriminator)
|
|
16938
17316
|
- 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
17317
|
- Plan tier vocabulary (the hosted plan tiers and the purchasable subset, shared by the Platform and the browser dashboard)
|
|
16940
17318
|
- Infrastructure alert vocabulary (the diagnostics alert types and severities the Platform mints and the dashboard and `kici` CLI render)
|
|
16941
17319
|
- Metric catalog (the generated Prometheus metric inventory, its naming policy, and metric-kind compatibility checks)
|
|
16942
|
-
- Bundler config (shared
|
|
17320
|
+
- 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
17321
|
|
|
16944
17322
|
> Source: `packages/engine/src/`
|
|
16945
17323
|
|
|
@@ -16959,7 +17337,7 @@ It also runs the **local dev plane** -- an on-demand, fully local execution stac
|
|
|
16959
17337
|
|
|
16960
17338
|
### `@kici-dev/core`
|
|
16961
17339
|
|
|
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.
|
|
17340
|
+
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
17341
|
|
|
16964
17342
|
> Source: `packages/core/src/`
|
|
16965
17343
|
|
|
@@ -17041,7 +17419,7 @@ KiCI uses three WebSocket layers for real-time communication.
|
|
|
17041
17419
|
|
|
17042
17420
|
### Platform ↔ Orchestrator
|
|
17043
17421
|
|
|
17044
|
-
The orchestrator connects outbound to the Platform WebSocket endpoint. After authentication (API key validated via SHA-256 hash lookup), the connection is used for webhook relay, execution telemetry (events, status, logs), source registration, and peer discovery.
|
|
17422
|
+
The orchestrator connects outbound to the Platform WebSocket endpoint. After authentication (API key validated via SHA-256 hash lookup), the connection is used for webhook relay, execution telemetry (events, status, logs), source registration, and peer discovery. Peer discovery is matchmaking only: the Platform pushes a `peer.update` membership list to every orchestrator sharing a routing key, and the orchestrators then connect to each other directly. Inter-orchestrator traffic such as `job.reroute` never transits the Platform.
|
|
17045
17423
|
|
|
17046
17424
|
### Orchestrator ↔ Orchestrator (P2P)
|
|
17047
17425
|
|