@kici-dev/compiler 0.1.3 → 0.1.6

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 CHANGED
@@ -22,7 +22,7 @@ import "./commands/index.js";
22
22
  import { Argument, Command, Option } from "commander";
23
23
  import pc from "picocolors";
24
24
  //#region src/cli.ts
25
- const version = "0.1.3";
25
+ const version = "0.1.6";
26
26
  const program = new Command();
27
27
  program.name("kici").description("KiCI workflow compiler").version(version);
28
28
  program.hook("preAction", () => {
@@ -50,7 +50,7 @@ program.command("fixture").addArgument(fixtureEventArg).description("Generate fi
50
50
  await fixtureCommand(event, options);
51
51
  });
52
52
  const runCommand = program.command("run").description("Execute workflows locally or remotely");
53
- runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").action(async (event, options) => {
53
+ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).action(async (event, options) => {
54
54
  if (options.pick && options.workflow) {
55
55
  console.error("Error: --pick is mutually exclusive with --workflow.");
56
56
  process.exit(2);
@@ -76,7 +76,9 @@ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open
76
76
  junit: options.junit,
77
77
  files: options.files,
78
78
  debug: options.debug,
79
- kiciDir: options.kiciDir
79
+ kiciDir: options.kiciDir,
80
+ inPlace: options.inPlace,
81
+ keep: options.keep
80
82
  });
81
83
  process.exit(success ? 0 : 1);
82
84
  });
@@ -48,16 +48,15 @@ async function oauthLogin(options) {
48
48
  const platformUrl = options.platformEndpoint || process.env.KICI_PLATFORM_URL || "";
49
49
  const issuer = process.env.KICI_OIDC_ISSUER || "";
50
50
  const clientId = process.env.KICI_OIDC_CLIENT_ID || "";
51
- const projectId = process.env.KICI_OIDC_PROJECT_ID || "";
52
51
  const missing = [];
53
52
  if (!platformUrl) missing.push("KICI_PLATFORM_URL");
54
53
  if (!issuer) missing.push("KICI_OIDC_ISSUER");
55
54
  if (!clientId) missing.push("KICI_OIDC_CLIENT_ID");
56
55
  if (missing.length > 0) {
57
56
  console.error(pc.red(`Error: missing required env var(s) for OAuth login: ${missing.join(", ")}`));
58
- console.error(pc.gray(" Set them to your Platform and Zitadel endpoints, for example:"));
57
+ console.error(pc.gray(" Set them to your Platform and IdP endpoints, for example:"));
59
58
  console.error(pc.gray(" export KICI_PLATFORM_URL=https://your-platform.example.com"));
60
- console.error(pc.gray(" export KICI_OIDC_ISSUER=https://your-zitadel.example.com"));
59
+ console.error(pc.gray(" export KICI_OIDC_ISSUER=https://your-idp.example.com"));
61
60
  console.error(pc.gray(" export KICI_OIDC_CLIENT_ID=<cli-client-id>"));
62
61
  return false;
63
62
  }
@@ -66,12 +65,11 @@ async function oauthLogin(options) {
66
65
  const useDeviceFlow = options.device || !browserCmdSet && isHeadless();
67
66
  const flowName = useDeviceFlow ? "device authorization" : "PKCE (browser)";
68
67
  console.log(pc.gray(` Using ${flowName} flow`));
69
- console.log(pc.cyan(`\n Step 2/4: Authenticating with ZITADEL (${flowName})...`));
68
+ console.log(pc.cyan(`\n Step 2/4: Authenticating with IdP (${flowName})...`));
70
69
  let accessToken;
71
70
  const oauthOpts = {
72
71
  issuer,
73
- clientId,
74
- projectId: projectId || void 0
72
+ clientId
75
73
  };
76
74
  if (useDeviceFlow) accessToken = await deviceFlow(oauthOpts);
77
75
  else accessToken = await pkceFlow(oauthOpts);
@@ -15,7 +15,7 @@ import { formatDuration, logger, toErrorMessage } from "@kici-dev/shared";
15
15
  * Looks up local history first, then fetches from the orchestrator for
16
16
  * up-to-date status and logs.
17
17
  */
18
- const CLI_VERSION = "0.1.3";
18
+ const CLI_VERSION = "0.1.6";
19
19
  /**
20
20
  * Show status and details of a test run.
21
21
  *
@@ -14,7 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  * in its sandbox process). No Rolldown bundling: host-repo imports and
15
15
  * transitive deps with dynamic import() resolve via Node's normal ESM loader
16
16
  * against the workspace's node_modules — the same module graph any other
17
- * `npx tsx` invocation would see. This keeps ops-style workflows (which
17
+ * `pnpm exec tsx` invocation would see. This keeps ops-style workflows (which
18
18
  * import repo-local modules like scripts/lib/*) working the same as minimal
19
19
  * workflows that only touch @kici-dev/sdk.
20
20
  */
@@ -449,6 +449,41 @@ The lock file approach means the orchestrator stays git-agnostic -- it only need
449
449
 
450
450
  ---
451
451
 
452
+ ## 5-minute quickstart
453
+
454
+ Source: https://kici.dev/docs/user/quickstart/
455
+
456
+ KiCI offers two equally-supported quickstart paths. Pick the one that fits your machine — both end with the same working pipeline (orchestrator + agent + your first workflow run visible in the dashboard).
457
+
458
+ ## Option A — Docker / Podman (recommended)
459
+
460
+ Two containers brought up with `docker compose up -d` (orchestrator + PostgreSQL), plus one short-lived agent container spawned per job by the container scaler. Minimal host setup, perfect for a laptop, home server, or a tiny VM. No need to install PostgreSQL or any other system service.
461
+
462
+ [Start with the Docker / Podman quickstart →](./quickstart/compose.md)
463
+
464
+ ## Option B — Bare-metal install
465
+
466
+ Native systemd services managed by `kici-admin orchestrator install` / `kici-admin agent install` — the orchestrator and agents run as native processes. The backing PostgreSQL runs as a single container by default (one `docker compose up -d`), or you can install it natively if you'd rather not run a container runtime at all. Best for a long-lived Linux host.
467
+
468
+ [Start with the bare-metal quickstart →](./quickstart/bare-metal.md)
469
+
470
+ ## Which should I pick?
471
+
472
+ | | Docker / Podman | Bare metal |
473
+ | ----------------- | ---------------------------------------- | ------------------------------------------------------------------------------------- |
474
+ | Host requirements | `docker` or `podman` with compose v2.20+ | systemd, Node.js 24+, PostgreSQL 18 (container — needs `docker`/`podman` — or native) |
475
+ | Time to first run | ~5 minutes | ~10 minutes |
476
+ | Upgrades | `docker compose pull` + restart | `kici-admin orchestrator restart` after `npm install -g kici-admin@latest` |
477
+ | Best for | Quick evaluation, ephemeral hosts | Long-lived production hosts |
478
+
479
+ If you're not sure, pick Docker / Podman.
480
+
481
+ ## Looking for the laptop-only path?
482
+
483
+ Both quickstarts deploy a real orchestrator + agent. If you only want to write a workflow and dry-run it on your laptop with no infrastructure, [Getting started](./getting-started.md) covers `kici test` and `kici run local` instead.
484
+
485
+ ---
486
+
452
487
  # Workflow patterns
453
488
 
454
489
  ## Basic workflow patterns
@@ -1337,7 +1372,7 @@ export default workflow('on-github-repo-push', {
1337
1372
  - No auto-clone — `packages/agent/src/checkout/git-clone.ts` uses GitHub App installation tokens to fetch the repo; a generic source has none. Either set `checkout: false` and clone yourself with a PAT/Deploy Key secret (same pattern as the Forgejo manual-clone example above), or keep the workflow self-contained.
1338
1373
  - No lock-file fetch — the orchestrator cannot fetch `.kici/kici.lock.json` at the pushed SHA via the GitHub API. The workflow must be pre-registered via the [registration model](../events.md#the-registration-model); ad-hoc per-commit workflow discovery that a GitHub App push gives you is not available.
1339
1374
  - No changed-files enrichment — `event.changedFiles` is empty. Use JSONPath `match` on `rawPayload.commits[*].added/modified/removed` if you need path filters.
1340
- - No check-run integration — KiCI cannot post commit statuses or Check Run results back to GitHub.
1375
+ - No check-run integration — KiCI cannot post Check Run results back to GitHub.
1341
1376
  - Workflow authors must use `genericWebhook()`, not `push()` / `pr()` / `webhook()` — the latter three only match events delivered through the native GitHub App provider.
1342
1377
 
1343
1378
  **When to use it anyway:** trigger-only workflows that don't need the cloned repo — posting Slack messages, kicking off external deploys, forwarding to downstream systems, or exposing GitHub repo events as `genericWebhook` for same-org [cross-source fan-out](../../architecture/webhooks/webhook-delivery.md#cross-source-delivery). For anything that compiles, tests, or checks code, install the GitHub App instead.
@@ -1390,7 +1425,7 @@ const build = step('build', {
1390
1425
 
1391
1426
  ## Examples repository
1392
1427
 
1393
- For more runnable examples, see the [examples/](https://github.com/kici-dev/kici/tree/master/examples) directory in the KiCI repository.
1428
+ For more runnable examples, see the [examples/](https://github.com/kici-dev/kici-public/tree/main/examples) directory in the KiCI repository.
1394
1429
 
1395
1430
  ## GitHub check run output
1396
1431
 
@@ -3452,7 +3487,7 @@ function genericWebhook(config: GenericWebhookConfigInput): GenericWebhookTrigge
3452
3487
 
3453
3488
  ```typescript
3454
3489
  interface GenericWebhookConfigInput {
3455
- source: string; // Required: webhook source identifier
3490
+ source: string; // Required: must match `--name` from `kici-admin source add generic`
3456
3491
  events?: string[]; // Filter by event types
3457
3492
  match?: Record<string, unknown>; // JSONPath payload matching
3458
3493
  not?: Record<string, unknown>; // Negative JSONPath filter
@@ -3873,7 +3908,7 @@ The KiCI CLI supports three authentication methods: browser-based OAuth (default
3873
3908
 
3874
3909
  The default `kici login` flow:
3875
3910
 
3876
- 1. Opens your default browser to the KiCI identity provider (ZITADEL)
3911
+ 1. Opens your default browser to the KiCI identity provider
3877
3912
  2. You authenticate in the browser
3878
3913
  3. The CLI receives a token via localhost callback
3879
3914
  4. A personal access token (PAT) is created and stored locally
@@ -4012,20 +4047,20 @@ The Platform exposes a versioned REST API under `/api/v1/*`. The same endpoints
4012
4047
 
4013
4048
  Every request to `/api/v1/*` carries an `Authorization: Bearer <token>` header. The Platform routes on the prefix:
4014
4049
 
4015
- | Prefix | Token type | Created via | Scope |
4016
- | ----------- | -------------------------------- | --------------------------------------- | ---------------- |
4017
- | `kici_pat_` | Personal access token | `kici login` or dashboard | User (cross-org) |
4018
- | `kici_sk_` | User API key | Dashboard → Settings → API keys | Org |
4019
- | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
4020
- | (other) | ZITADEL JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
4050
+ | Prefix | Token type | Created via | Scope |
4051
+ | ----------- | ----------------------------- | --------------------------------------- | ---------------- |
4052
+ | `kici_pat_` | Personal access token | `kici login` or dashboard | User (cross-org) |
4053
+ | `kici_sk_` | User API key | Dashboard → Settings → API keys | Org |
4054
+ | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
4055
+ | (other) | OIDC JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
4021
4056
 
4022
- JWT and opaque OIDC tokens are validated against your ZITADEL issuer (JWKS for JWTs, `/oidc/v1/userinfo` for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](../architecture/security/rbac.md#authentication-methods) for the full model.
4057
+ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWKS for JWTs, the issuer's UserInfo endpoint for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](../architecture/security/rbac.md#authentication-methods) for the full model.
4023
4058
 
4024
4059
  > **Note:** `kici_ok_` keys are **not** for the HTTP API — they authenticate orchestrator-to-Platform WebSocket connections only. Use `kici_sk_` (or `kici_pat_`) for HTTP calls.
4025
4060
 
4026
4061
  ### Permissions
4027
4062
 
4028
- Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 13 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
4063
+ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 14 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
4029
4064
 
4030
4065
  ### Configurable surfaces
4031
4066
 
@@ -4232,6 +4267,8 @@ kici run local [event] [options]
4232
4267
  | `--junit <path>` | none | Output JUnit XML result to file |
4233
4268
  | `--debug` | `false` | Verbose internals |
4234
4269
  | `--kici-dir <path>` | `.kici` | Path to .kici directory |
4270
+ | `--in-place` | `false` | Run against the real working directory instead of an isolated tmp checkout (see "Execution isolation" below) |
4271
+ | `--keep` | `false` | Always retain the isolated tmp checkout (default: keep only on failure) |
4235
4272
 
4236
4273
  **Interactive workflow selection (`--pick` / `-p`):**
4237
4274
 
@@ -4265,6 +4302,26 @@ Coordination is local only — running the same workflow on two different machin
4265
4302
 
4266
4303
  The `SIGTERM`-to-`SIGKILL` grace window defaults to 30 000 ms. Override it with the `KICI_LOCAL_LOCK_KILL_GRACE_MS` environment variable (positive integer, milliseconds) when iterating on workflows that need longer to clean up on cancellation.
4267
4304
 
4305
+ **Execution isolation:**
4306
+
4307
+ By default, `kici run local` executes steps inside an **isolated tmp checkout** rather than against your real working directory. Any file a step writes, builds, or deletes — and any `git` mutation a step performs — lands in that throwaway copy, so casual local runs never touch your tree.
4308
+
4309
+ What gets materialized into the isolated checkout has full parity with what `kici run remote` reconstructs: your current working tree minus gitignored files, with `.kiciignore` applied to local changes, over a real `.git` directory. Concretely, the checkout is built from a clone pinned to your current `HEAD`, with your local overlay (modified, staged, and untracked-but-not-ignored files) copied on top and locally-deleted files removed. Workflows that read git metadata work because the `.git` directory is present and pinned to your `HEAD`.
4310
+
4311
+ The path is logged at run start (for example, `running in /tmp/kici-run-ab12cd`) so you can inspect it.
4312
+
4313
+ Cleanup policy:
4314
+
4315
+ - On a fully successful run, the isolated checkout is removed.
4316
+ - On failure, it is retained and its path is logged so you can inspect the failed state.
4317
+ - `--keep` always retains it, even on success.
4318
+
4319
+ Set the `KICI_RUN_DIR` environment variable to place the isolated checkout under a base directory other than the system temp directory.
4320
+
4321
+ Secrets are always sourced from your real `.kici/` directory, not from the isolated checkout. Gitignored secret files (such as `.kici/.env.local` and `.kici/secrets.yaml`) are never copied into the checkout, so a step that reads a secret still gets it from the original location.
4322
+
4323
+ 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.
4324
+
4268
4325
  **Examples:**
4269
4326
 
4270
4327
  ```bash
@@ -4453,7 +4510,7 @@ kici test push --files src/index.ts --files README.md
4453
4510
 
4454
4511
  Authenticate with KiCI via browser-based OAuth (default) or API key (`--token`).
4455
4512
 
4456
- By default, `kici login` opens your browser for ZITADEL authentication using PKCE. In headless environments (SSH, CI, containers), it automatically switches to the RFC 8628 device authorization flow where you visit a URL and enter a code.
4513
+ By default, `kici login` opens your browser for OIDC authentication using PKCE. In headless environments (SSH, CI, containers), it automatically switches to the RFC 8628 device authorization flow where you visit a URL and enter a code.
4457
4514
 
4458
4515
  After OAuth, the CLI exchanges the OIDC token for a personal access token (PAT) stored in the config directory (`~/.kici/config` by default, overridable with `KICI_CONFIG_DIR`).
4459
4516
 
@@ -5318,7 +5375,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
5318
5375
 
5319
5376
  | Field | Description |
5320
5377
  | --------------- | ------------------------------------------------------------------------------------------------------------------- |
5321
- | `schemaVersion` | Lock file schema version (currently 11). Incremented on breaking format changes. |
5378
+ | `schemaVersion` | Lock file schema version (currently 12). Incremented on breaking format changes. |
5322
5379
  | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
5323
5380
  | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
5324
5381
  | `lockfileHash` | SHA-256 of `.kici/package-lock.json`, used as the dependency cache key. Omitted when no package lock file exists. |
@@ -6068,6 +6125,7 @@ The left sidebar provides persistent navigation across all org-scoped pages:
6068
6125
  - **Secrets** -- secret scope management with environment bindings
6069
6126
  - **Approval queue** -- held runs pending approval (shows a badge with pending count)
6070
6127
  - **Activity** -- federated forensic log merging upstream tenant-plane mutations and orchestrator reads (`access_log`) into one chronological stream
6128
+ - **DLQ** -- dead-letter queue of internal events whose dispatch retries were exhausted (shows a badge with the current depth)
6071
6129
  - **Settings** -- organization settings with tabbed sub-pages
6072
6130
 
6073
6131
  The sidebar footer shows the WebSocket connection indicator, your user profile, UTC/local time toggle, theme toggle, and a collapse button.
@@ -6445,7 +6503,7 @@ Each member's linked provider accounts (e.g. GitHub) are also visible here.
6445
6503
 
6446
6504
  <!-- help:settings-roles#settings -->
6447
6505
 
6448
- Roles define granular permissions across 13 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
6506
+ Roles define granular permissions across 14 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
6449
6507
 
6450
6508
  Create custom roles to restrict what team members can do, or use the built-in **Owner** role for full access.
6451
6509
 
@@ -6521,6 +6579,16 @@ When you hit the cap, new relayed webhooks are rejected with `429 Plan limit rea
6521
6579
 
6522
6580
  <!-- /help:settings-billing-relayed-webhooks -->
6523
6581
 
6582
+ <!-- help:settings-billing-currency#settings -->
6583
+
6584
+ Switch the prices shown on the tier cards between US dollars and euros. The choice you pick here is also the currency Stripe charges in when you click "Upgrade".
6585
+
6586
+ The default is detected from your browser language. EU, EFTA, and UK locales default to euros; everywhere else defaults to dollars.
6587
+
6588
+ Your choice persists in a 90-day cookie (`kici_pricing_currency`), so it survives across reloads and applies on every billing page.
6589
+
6590
+ <!-- /help:settings-billing-currency -->
6591
+
6524
6592
  <!-- help:billing-payment-failure#settings -->
6525
6593
 
6526
6594
  This banner appears when your organization's latest payment to Stripe has failed. Your subscription remains active during the retry period, but you should update your payment method promptly to avoid service interruption.
@@ -6549,6 +6617,23 @@ Click a row's run target to jump straight to the run detail page.
6549
6617
 
6550
6618
  <!-- /help:activity-filters -->
6551
6619
 
6620
+ ## DLQ
6621
+
6622
+ The DLQ (dead-letter queue) page lists internal events whose dispatch attempts were exhausted (or that hit a non-retryable error). Each row shows when the event landed in the DLQ, the event name, the attempt count, the failure reason, and the last error message.
6623
+
6624
+ <!-- help:dlq#dlq -->
6625
+
6626
+ The DLQ holds events your org emitted that could not be dispatched within the retry budget. The sidebar badge shows the current depth so you can spot a building backlog without opening the page.
6627
+
6628
+ Per-row actions (visible when you have `event_dlq:write`):
6629
+
6630
+ - **Retry:** clears the DLQ flag and re-publishes the event. A healthy orchestrator picks it up immediately.
6631
+ - **Discard:** permanently deletes the row. Use when the payload is corrupt or the routing target no longer exists.
6632
+
6633
+ Members with only `event_dlq:read` see the list but cannot retry or discard. Org owners have both actions by default.
6634
+
6635
+ <!-- /help:dlq -->
6636
+
6552
6637
  <!-- help:settings-ci-trust#settings -->
6553
6638
 
6554
6639
  CI trust policy controls how your organization handles PR-triggered runs from different contributor types. Configure the default trust level for unknown contributors and set per-member overrides to control who can run workflows with full secrets access.
@@ -6643,6 +6728,22 @@ For each endpoint you can:
6643
6728
 
6644
6729
  <!-- /help:settings-webhooks -->
6645
6730
 
6731
+ <!-- help:settings-security-dashboard-policy#settings -->
6732
+
6733
+ Read-only view of the orchestrator's dashboard-write policy.
6734
+
6735
+ Each row toggles one mutating dashboard action — setting a secret, approving a held run, retrying a dead-lettered webhook, and so on. The orchestrator operator decides which actions stay on the dashboard and which become **CLI-only**. The dashboard cannot change the policy itself — that's the point: disabled actions stay out of the SaaS Platform's trust path.
6736
+
6737
+ Manage the policy with:
6738
+
6739
+ - **Show the full policy:** `kici-admin org-settings dashboard-writes show`
6740
+ - **Disable an operation:** `kici-admin org-settings dashboard-writes set --op <name>=false`
6741
+ - **Reset to permissive defaults:** `kici-admin org-settings dashboard-writes reset`
6742
+
6743
+ The summary strip at the top shows total / enabled / disabled counts plus whether your orchestrator is currently connected. A disconnected orchestrator means the page falls back to the cached policy from the most recent connection.
6744
+
6745
+ <!-- /help:settings-security-dashboard-policy -->
6746
+
6646
6747
  <!-- help:settings-webhooks-delivery-log#settings -->
6647
6748
 
6648
6749
  The delivery log shows recent webhook deliveries for an endpoint, including the HTTP status code, number of retry attempts, and the event payload.
@@ -6698,6 +6799,7 @@ The settings page (`/orgs/:customerId/settings`) uses a tabbed layout:
6698
6799
  9. **Global workflows** -- org-level security knobs for cross-repo workflows (visible with `org_settings:read` permission)
6699
6800
  10. **Webhooks** -- outbound webhook endpoint management with delivery logs and test ping
6700
6801
  11. **Event log** -- inbound webhook delivery log (visible with `event_log:read` permission)
6802
+ 12. **Security** -- read-only view of the orchestrator's dashboard-write policy matrix (visible with `org_settings:read` permission)
6701
6803
 
6702
6804
  Audit-log-style entries are no longer a settings tab; they live on the dedicated **Activity** page accessible from the sidebar.
6703
6805
 
@@ -6793,6 +6895,28 @@ Organizations are the top-level container for your CI/CD resources. Each org has
6793
6895
 
6794
6896
  <!-- /help:orgs-list -->
6795
6897
 
6898
+ <!-- help:orchestrators-list#orchestrators -->
6899
+
6900
+ The Orchestrators page lists every orchestrator currently connected to this org, keyed by **cluster name**. Each row shows:
6901
+
6902
+ - **Cluster** — the human-friendly cluster name set on the orch via `kici-admin cluster-name set <name>`, or an auto-generated `cluster-<6hex>` if no operator has renamed it.
6903
+ - **Role** — `coordinator` (talks to Platform directly) or `worker` (relays through a coordinator).
6904
+ - **Version**, **mode**, **routing keys**, and **last heartbeat**.
6905
+
6906
+ Click a cluster to drill into its per-orch surfaces (security policy, environments, secrets, DLQ, registrations, global workflows). Different clusters in the same org can have different settings — this page is the entry point that lets you pick which cluster you're configuring.
6907
+
6908
+ <!-- /help:orchestrators-list -->
6909
+
6910
+ <!-- help:orchestrators-scope#orchestrators -->
6911
+
6912
+ Every panel inside this view scopes to the named cluster. Settings shown here come from that orchestrator's own database — a sibling orchestrator in the same org may have a different security policy, different environments, and different secrets.
6913
+
6914
+ When the cluster shows **disconnected**, the orch is offline and its current state can't be queried. Most child pages will return 404 in that state; return to the orchestrator list to find a connected cluster.
6915
+
6916
+ To rename a cluster, run `kici-admin cluster-name set <new>` on the orchestrator host and restart the orch service so the new name reaches Platform on the next `source.register`.
6917
+
6918
+ <!-- /help:orchestrators-scope -->
6919
+
6796
6920
  ## Workflows
6797
6921
 
6798
6922
  The workflows page (`/orgs/:customerId/workflows`) shows permanently registered workflows listening for events. It displays a filterable table with columns for workflow name, repository, trigger types, last triggered time, next fire time (for scheduled workflows), source repos, and actions.
@@ -6841,6 +6965,22 @@ The secrets page (`/orgs/:customerId/secrets`) provides a scope-centric view of
6841
6965
 
6842
6966
  Permission-gated: `secrets:read` to view scopes, `secrets:write` to add or delete secrets, `environments:write` to modify environment bindings.
6843
6967
 
6968
+ ### Where secrets live
6969
+
6970
+ Secret values are stored in the orchestrator's secret store and authorized through the orchestrator's RBAC. The dashboard surfaces secret **names** and scope membership for every secret regardless of where the value was entered.
6971
+
6972
+ Whether secret **values** can be set from the dashboard depends on the orchestrator's [dashboard-write policy](/operator/security/dashboard-write-policy):
6973
+
6974
+ - **Permissive (default):** the "Add secret" and "Edit value" controls accept plaintext directly in the dashboard. This is how a typical SaaS CI tool works and is the right default for small teams.
6975
+ - **`secrets.set` disabled by policy:** the controls render with a lock icon, grayed out. Hovering shows the exact `kici-admin secret set` invocation needed; a copy button puts it on the clipboard. The control is inert — the dashboard issues no mutating request. Use the CLI to enter values; the dashboard refreshes within ~30 seconds and shows the new secret name.
6976
+
6977
+ The policy state is visible at three layers in the UI:
6978
+
6979
+ - A **lock-icon prefix** on every disabled control, with a per-control CLI hint.
6980
+ - A **per-page banner** on any page containing at least one disabled operation, listing every disabled op on that page and its CLI equivalent.
6981
+
6982
+ The Security policy page (Settings → Security → Dashboard policy) renders the full 23-row read-only matrix with the current state and the `kici-admin` command for each row. The policy itself cannot be changed from the dashboard — the orchestrator operator manages it via `kici-admin org-settings dashboard-writes`. See [Dashboard-write policy](/operator/security/dashboard-write-policy) for the operator-side details.
6983
+
6844
6984
  ## Approval queue
6845
6985
 
6846
6986
  The approval queue page (`/orgs/:customerId/approval-queue`) shows held runs that are pending approval. Runs can be held due to environment protection rules (required reviewers, wait timers). The page supports filtering by status (pending, approved, rejected, expired) and provides approve/reject actions for users with `environments:write` permission. Users with `environments:admin` permission can skip wait timers.
@@ -7067,7 +7207,7 @@ kici login --token <<< "$KICI_API_KEY"
7067
7207
  Point the CLI to a self-hosted or testing OIDC provider:
7068
7208
 
7069
7209
  ```bash
7070
- export KICI_OIDC_ISSUER=https://your-zitadel.example.com
7210
+ export KICI_OIDC_ISSUER=https://your-idp.example.com
7071
7211
  export KICI_OIDC_CLIENT_ID=your-client-id
7072
7212
  export KICI_OIDC_PROJECT_ID=your-project-id
7073
7213
  export KICI_PLATFORM_URL=https://your-platform.example.com
@@ -7510,7 +7650,7 @@ genericWebhook({
7510
7650
 
7511
7651
  **Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
7512
7652
 
7513
- Generic webhook sources must be created by an operator via the admin API before events can be received. See [Operator guide: event routing](../operator/event-routing.md) for setup instructions.
7653
+ The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](../operator/event-routing.md) for setup instructions.
7514
7654
 
7515
7655
  ### Schedule events
7516
7656
 
@@ -7658,7 +7798,7 @@ KiCI's event router delivers every accepted event with **at-least-once** semanti
7658
7798
  is released on failure) and the event is automatically retried.
7659
7799
  - The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
7660
7800
  up to 5 attempts before the event lands in the **DLQ** (dead-letter queue).
7661
- Operators triage DLQ entries via `kici-admin event-dlq list / retry / discard`.
7801
+ Operators triage DLQ entries via `kici-admin event-dlq list / count / retry / discard`.
7662
7802
 
7663
7803
  **What this means for workflow authors:**
7664
7804
 
@@ -8151,6 +8291,60 @@ Secrets are managed per-environment in the orchestrator (see [operator docs](/op
8151
8291
 
8152
8292
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
8153
8293
 
8294
+ ## Where secret values come from
8295
+
8296
+ Secret values are written either through the dashboard or through `kici-admin` running against the orchestrator. The orchestrator operator decides — per organization — which surface accepts secret writes. From the workflow author's perspective, the resolution path at run time is identical either way; the difference is where you (or your ops team) **enter** the value.
8297
+
8298
+ ### Default — dashboard or CLI
8299
+
8300
+ A fresh orchestrator starts in **permissive** mode: both surfaces are available.
8301
+
8302
+ - **Dashboard:** Settings → Secrets → pick a scope → enter the secret name and value.
8303
+ - **CLI:** `kici-admin secret set --scope <scope> <KEY>` against the orchestrator's HTTP admin API.
8304
+
8305
+ Use whichever fits the workflow — most small teams stay on the dashboard; ops engineers and CI scripts use the CLI.
8306
+
8307
+ ### When the operator has disabled dashboard writes
8308
+
8309
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](/operator/security/dashboard-write-policy). When that flip is on:
8310
+
8311
+ - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Clicking them shows a tooltip with the exact `kici-admin secret set` invocation needed.
8312
+ - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
8313
+ - `kici-admin secret set` becomes the single entry point for new and updated secret values.
8314
+
8315
+ This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, environment bindings).
8316
+
8317
+ ### CLI input modes
8318
+
8319
+ `kici-admin secret set` accepts five input modes — pick the one that fits your workflow:
8320
+
8321
+ ```bash
8322
+ # Interactive prompt (default when stdin is a TTY). No echo, no shell history.
8323
+ kici-admin secret set --scope production DB_PASSWORD --prompt
8324
+
8325
+ # Pipe from another tool (default when stdin is not a TTY).
8326
+ pass show prod/db | kici-admin secret set --scope production DB_PASSWORD --from-stdin
8327
+
8328
+ # Read from a file (handy after `sops -d` to a tmpfile).
8329
+ kici-admin secret set --scope production DB_PASSWORD --from-file ./db.pass
8330
+
8331
+ # Read from a named environment variable (CI-friendly).
8332
+ KICI_SECRET_VALUE=$(my-secrets-fetcher prod db) \
8333
+ kici-admin secret set --scope production DB_PASSWORD --from-env KICI_SECRET_VALUE
8334
+
8335
+ # Direct argv — discouraged. Prints a stderr warning ("visible in shell history").
8336
+ kici-admin secret set --scope production DB_PASSWORD --value "<plaintext>"
8337
+ ```
8338
+
8339
+ Two cross-cutting flags help every mode:
8340
+
8341
+ - `--confirm-fingerprint <hex>` — pre-compute SHA-256 of the value and pass it. The CLI rejects the call if the value's fingerprint doesn't match. Catches paste corruption.
8342
+ - `--dry-run` — parse and validate the value, print `[dry-run] would set <key> in scope <scope> sha256=<hex>`, exit without writing.
8343
+
8344
+ `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
8345
+
8346
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
8347
+
8154
8348
  ## Accessing secrets
8155
8349
 
8156
8350
  Use `ctx.secrets.get(key)` to retrieve a secret value. The method is async to support process-level step isolation in future versions.
@@ -8399,11 +8593,14 @@ repos where you can't install an App.
8399
8593
  https://<platform-host>/webhook/<orgId>/github
8400
8594
  ```
8401
8595
 
8402
- For self-hosted independent / hybrid orchestrators the same path
8403
- serves directly on the orchestrator's public hostname. The `<orgId>`
8404
- segment is the KiCI organization ID the source belongs to; the
8405
- `<appId>` is discovered from `X-GitHub-Hook-Installation-Target-ID`
8406
- at request time and is _not_ part of the URL.
8596
+ GitHub App webhooks are always delivered to this Platform endpoint and
8597
+ relayed to your orchestrator over its outbound connection — platform and
8598
+ hybrid orchestrators both receive GitHub events this way. Independent-mode
8599
+ orchestrators have no Platform connection and therefore no GitHub-App
8600
+ ingress; use a generic webhook source instead. The `<orgId>` segment is
8601
+ the KiCI organization ID the source belongs to; the `<appId>` is
8602
+ discovered from `X-GitHub-Hook-Installation-Target-ID` at request time and
8603
+ is _not_ part of the URL.
8407
8604
 
8408
8605
  4. **Set the webhook secret.** Generate a random hex string (e.g.
8409
8606
  `openssl rand -hex 32`) and save it for step 4 of the orchestrator
@@ -8412,18 +8609,38 @@ repos where you can't install an App.
8412
8609
 
8413
8610
  5. **Pick permissions.** Minimum required:
8414
8611
 
8415
- | Scope | Access | Why |
8416
- | ----------------------------- | ------------ | --------------------------------------- |
8417
- | Repository -> Contents | Read | Clone the repo to read the lock file |
8418
- | Repository -> Metadata | Read (auto) | Default for every App |
8419
- | Repository -> Pull requests | Read | Match `pull_request` triggers |
8420
- | Repository -> Checks | Read & write | Post KiCI's enriched Check runs |
8421
- | Repository -> Commit statuses | Read & write | (optional) Legacy commit-status posting |
8612
+ | Scope | Access | Why |
8613
+ | --------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
8614
+ | Repository -> Contents | Read | Clone the repo to read the lock file |
8615
+ | Repository -> Metadata | Read (auto) | Default for every App; also lets KiCI look up a pull-request author's repository access level for CI trust |
8616
+ | Repository -> Pull requests | Read | Match `pull_request` triggers |
8617
+ | Repository -> Checks | Read & write | Post KiCI's enriched Check runs |
8618
+ | Organization -> Members | Read | (optional, org installs) Receive `organization` / `membership` / `team` events so KiCI's CI-trust permission cache invalidates promptly on access changes |
8619
+
8620
+ The first four rows cover the core flow (clone, trigger matching,
8621
+ Check runs). The **Organization -> Members** row is only relevant if
8622
+ you use [CI trust tiers](../../architecture/security/ci-security.md)
8623
+ on an org-level install — see the event note below.
8422
8624
 
8423
8625
  6. **Subscribe to events.** At minimum: `push`, `pull_request`,
8424
8626
  `check_run`, `check_suite`. Add others (`issues`, `release`, ...) if
8425
8627
  your workflows use those triggers.
8426
8628
 
8629
+ **For CI trust (optional but recommended on org installs):** also
8630
+ subscribe to `member`, `organization`, `membership`, and `team`.
8631
+ KiCI caches each pull-request author's repository access level (used
8632
+ to decide whether workflow changes take effect immediately or are
8633
+ held for approval — see
8634
+ [CI security](../../architecture/security/ci-security.md)). These
8635
+ events let the orchestrator drop stale cache entries the moment a
8636
+ contributor's access changes. They are not required for correctness:
8637
+ without them the cache simply ages out on its own 15-minute TTL, so a
8638
+ permission change can take up to 15 minutes to take effect. The
8639
+ `organization` / `membership` / `team` events require the
8640
+ **Organization -> Members** read permission and an org-level
8641
+ installation; `member` is a repository event covered by the default
8642
+ Metadata permission.
8643
+
8427
8644
  7. **Generate a private key.** Scroll to the bottom of the App settings
8428
8645
  and click _Generate a private key_. A `.pem` file downloads —
8429
8646
  store it safely; you cannot redownload it.
@@ -8449,10 +8666,21 @@ kici-admin --url http://<orchestrator-host>:4000 --token $KICI_BOOTSTRAP_ADMIN_T
8449
8666
  --webhook-secret <the-webhook-secret-from-step-4>
8450
8667
  ```
8451
8668
 
8452
- The command returns the routing key (always `github:<appId>`). The
8453
- private key and webhook secret are stored encrypted in the orchestrator
8454
- database under `KICI_SECRET_KEY`; no restart needed — the orchestrator
8455
- accepts webhooks from this App immediately.
8669
+ The command prints the routing key (always `github:<appId>`) and the public
8670
+ webhook URL to paste into the GitHub App's "Webhook URL" field:
8671
+
8672
+ ```
8673
+ Source added: github:<appId> (my-org)
8674
+ Webhook URL: https://<platform-host>/webhook/<orgId>/github
8675
+ ↳ Paste this into your GitHub App's "Webhook URL" field.
8676
+ ```
8677
+
8678
+ When the orchestrator runs in independent mode (no Platform connection) the
8679
+ URL line reads `(unavailable — this orchestrator runs in independent mode)`,
8680
+ because GitHub-App ingress is Platform-relayed. The private key and webhook
8681
+ secret are stored encrypted in the orchestrator database under
8682
+ `KICI_SECRET_KEY`; no restart needed — the orchestrator accepts webhooks from
8683
+ this App immediately.
8456
8684
 
8457
8685
  **Secret input modes** (for `--private-key` and `--webhook-secret`):
8458
8686
 
@@ -9529,13 +9757,13 @@ GitHub Platform Orchestrator Agent
9529
9757
 
9530
9758
  5. **Agent execution:** The agent wraps each `onJobDispatch` callback in `requestContext.run()` with `requestId`, `runId`, and `jobId`. A trace header is printed once at job start. All subsequent log lines carry all trace fields automatically.
9531
9759
 
9532
- 6. **Check run summaries:** GitHub Check Run updates include `Trace: <requestId> | Run: <runId>` in the summary text, giving operators a direct link from GitHub UI to ELK queries.
9760
+ 6. **Check run summaries:** GitHub Check Run updates include `Trace: <requestId> | Run: <runId>` in the summary text, giving operators a direct link from GitHub UI to Loki queries.
9533
9761
 
9534
9762
  ### Implementation
9535
9763
 
9536
- Trace propagation uses Node.js `AsyncLocalStorage` from `@kici-dev/shared`. A logger format (`traceContextFormat`) reads the current context and injects fields into every JSON log line -- no changes needed at individual call sites.
9764
+ Trace propagation uses Node.js `AsyncLocalStorage` from `@kici-dev/shared`. A logger format reads the current context and injects fields into every JSON log line -- no changes needed at individual call sites.
9537
9765
 
9538
- Tier identification is handled at the infrastructure level: `kici.service` (populated by Filebeat from the container name) identifies which service produced the log. For agent logs forwarded through the orchestrator's stdout, the `app.service` field (set to `'agent'` by the log forwarder) identifies the originating tier.
9766
+ Tier identification is handled at the infrastructure level: the `service` Loki label (set by Grafana Alloy from the systemd unit / log source) identifies which service produced the log (`platform`, `orchestrator`, `agent`, etc.). For agent logs forwarded through the orchestrator's stdout, the parsed JSON also carries an inner `service: 'agent'` field — query both with `{service="orchestrator"} | json | service="agent"` to disambiguate.
9539
9767
 
9540
9768
  ## Output chaining data flow
9541
9769
 
@@ -9633,7 +9861,7 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
9633
9861
  - [State machine](./execution/state-machine.md) -- job execution state transitions
9634
9862
  - [Webhook delivery](./webhooks/webhook-delivery.md) -- detailed webhook processing pipeline
9635
9863
  - [Operator: dependency caching](../operator/dependency-caching.md) -- configuration guide
9636
- - [Operator: monitoring & tracing](../operator/observability/monitoring.md) -- trace fields and ELK queries
9864
+ - [Operator: monitoring & tracing](../operator/observability/monitoring.md) -- trace fields and Loki queries
9637
9865
  - [Operator: event routing & generic webhooks](../operator/event-routing.md) -- generic source setup and trust management
9638
9866
  - [SDK reference: output chaining](../user/sdk/core.md#output-chaining) -- user-facing output chaining API
9639
9867
 
@@ -9693,7 +9921,7 @@ Per-source secrets enable multi-tenant Platform with proper isolation. Each cust
9693
9921
 
9694
9922
  **Decision:** Webhooks are routed by provider-scoped routing key (e.g., `github:12345`) derived from the `X-GitHub-Hook-Installation-Target-ID` header. One routing key maps to one orchestrator connection.
9695
9923
 
9696
- App ID routing provides a clean 1:1 mapping between a customer's GitHub App and their orchestrator. The orchestrator extracts the `installation_id` from the webhook payload body for GitHub API calls (fetching lock files, setting commit statuses).
9924
+ App ID routing provides a clean 1:1 mapping between a customer's GitHub App and their orchestrator. The orchestrator extracts the `installation_id` from the webhook payload body for GitHub API calls (fetching lock files, posting check runs).
9697
9925
 
9698
9926
  **Alternative:** Route by Installation ID (more granular, one App can have many installations). Rejected because it adds complexity without clear benefit -- most customers use one App with one or a few installations, and the orchestrator can handle multiple installations internally.
9699
9927
 
@@ -9885,7 +10113,7 @@ Shared utilities used across packages. Provides `initZx()` for zx initialization
9885
10113
 
9886
10114
  ### Dashboard
9887
10115
 
9888
- Web UI for KiCI. A browser single-page application that provides the operator dashboard with execution run listing, run detail views, real-time log streaming, settings management, and keyboard shortcut support. Authenticates via OIDC against ZITADEL and communicates with the Platform REST-over-WebSocket API.
10116
+ Web UI for KiCI. A browser single-page application that provides the operator dashboard with execution run listing, run detail views, real-time log streaming, settings management, and keyboard shortcut support. Authenticates via OIDC against the identity provider and communicates with the Platform REST-over-WebSocket API.
9889
10117
 
9890
10118
  ### `kici` (wrapper)
9891
10119
 
@@ -9961,7 +10189,7 @@ The agent connects outbound to the orchestrator WebSocket endpoint. After regist
9961
10189
 
9962
10190
  ## Authentication and multi-tenancy
9963
10191
 
9964
- KiCI uses application-level tenant isolation. The Platform dashboard API accepts three authentication methods (PATs, API keys, JWTs) and enforces org membership on every `/api/v1/orgs/:customerId/*` request through `orgContextMiddleware`. Triple-auth wiring, the ZITADEL identity provider integration, and the API key permission matrix are SaaS-Platform specifics covered in the internal docs.
10192
+ KiCI uses application-level tenant isolation. The Platform dashboard API accepts three authentication methods (PATs, API keys, JWTs) and enforces org membership on every `/api/v1/orgs/:customerId/*` request.
9965
10193
 
9966
10194
  ## See also
9967
10195