@kici-dev/compiler 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -4
- package/dist/commands/docs.js +2 -2
- package/dist/commands/feedback.d.ts +23 -1
- package/dist/commands/feedback.js +86 -9
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/local.d.ts +13 -5
- package/dist/commands/local.js +19 -8
- package/dist/commands/report/identity.js +1 -1
- package/dist/commands/run-banner.d.ts +1 -1
- package/dist/commands/run-banner.js +1 -1
- package/dist/commands/run-routed.js +3 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/runs/logs.js +3 -2
- package/dist/commands/verify-attestation.js +1 -1
- package/dist/llm-context/llms-architecture.txt +9 -7
- package/dist/llm-context/llms-cli-remote.txt +27 -12
- package/dist/llm-context/llms-cli.txt +6 -4
- package/dist/llm-context/llms-features-execution.txt +9 -9
- package/dist/llm-context/llms-features.txt +452 -133
- package/dist/llm-context/llms-full.txt +561 -197
- package/dist/llm-context/llms-getting-started.txt +34 -17
- package/dist/llm-context/llms-providers.txt +2 -2
- package/dist/llm-context/llms-sdk-runtime.txt +16 -2
- package/dist/llm-context/llms-sdk.txt +7 -12
- package/dist/llm-context/llms.txt +7 -6
- package/dist/local-plane/orchestrator-process.d.ts +4 -5
- package/dist/local-plane/orchestrator-process.js +5 -1
- package/dist/local-plane/paths.d.ts +1 -0
- package/dist/local-plane/paths.js +1 -0
- package/dist/local-plane/plane-log.d.ts +27 -0
- package/dist/local-plane/plane-log.js +39 -0
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/local-plane/plane-trigger.d.ts +28 -0
- package/dist/local-plane/plane-trigger.js +57 -2
- package/dist/local-plane/postgres.js +9 -6
- package/dist/local-plane/run-follow.js +2 -1
- package/dist/remote/output/streaming.d.ts +12 -0
- package/dist/remote/output/streaming.js +20 -1
- package/dist/remote/platform-client.d.ts +2 -0
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +9 -7
- package/dist/templates/package-json.d.ts +9 -7
- package/dist/templates/package-json.js +11 -9
- package/dist/templates/workflows/hello-world.ts +1 -1
- package/dist/templates/workflows/pr-checks.ts +2 -2
- package/dist/test-runner/job-executor.js +3 -2
- package/dist/types.d.ts +12 -35
- package/dist/types.js +2 -12
- package/dist/types.test-d.d.ts +2 -0
- package/dist/types.test-d.js +63 -0
- package/dist/workflows/hello-world.ts +1 -1
- package/dist/workflows/pr-checks.ts +2 -2
- package/package.json +15 -15
- package/sbom.spdx.json +1303 -1483
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# KiCI Workflow features: access and approval
|
|
2
2
|
|
|
3
|
-
This bundle covers: Deployment contexts, scoped secrets, private registries, approvals, provenance, dashboard and account access.
|
|
3
|
+
This bundle covers: Deployment contexts, scoped secrets, private registries, approvals, workload identity, provenance, dashboard and account access.
|
|
4
4
|
|
|
5
5
|
## Account and sign-in
|
|
6
6
|
|
|
@@ -557,13 +557,12 @@ Hold a job whose run came from a fork:
|
|
|
557
557
|
Minimum trust: trusted
|
|
558
558
|
```
|
|
559
559
|
|
|
560
|
-
| Value | Effect
|
|
561
|
-
| --------- |
|
|
562
|
-
| `trusted` | Holds a run whose ref came from a fork
|
|
563
|
-
|
|
|
564
|
-
| (unset) | No trust-based gating |
|
|
560
|
+
| Value | Effect |
|
|
561
|
+
| --------- | -------------------------------------- |
|
|
562
|
+
| `trusted` | Holds a run whose ref came from a fork |
|
|
563
|
+
| (unset) | No trust-based gating |
|
|
565
564
|
|
|
566
|
-
|
|
565
|
+
Trust comes from the git ref, and that judgement has two answers: a ref in your repository is `trusted`, a ref from a fork is `unknown`.
|
|
567
566
|
|
|
568
567
|
When the gate holds a job, it enters the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
|
|
569
568
|
|
|
@@ -718,6 +717,439 @@ This guide is split across the following pages:
|
|
|
718
717
|
|
|
719
718
|
---
|
|
720
719
|
|
|
720
|
+
## Workload identity with OIDC
|
|
721
|
+
|
|
722
|
+
Source: https://docs.kici.dev/user/oidc/
|
|
723
|
+
|
|
724
|
+
A job step can prove **which run it is** to an outside service without a stored
|
|
725
|
+
secret. The orchestrator mints a short-lived OIDC identity token for the step,
|
|
726
|
+
signs it with its own key, and the service verifies it against the
|
|
727
|
+
orchestrator's public keys. Your repository holds no long-lived credential for
|
|
728
|
+
that service; the exchanged credential exists only inside the step.
|
|
729
|
+
|
|
730
|
+
The orchestrator is the OIDC issuer. It publishes the discovery document and
|
|
731
|
+
the key set under `KICI_ORCHESTRATOR_PROVENANCE_ISSUER`, and the service you
|
|
732
|
+
exchange with must be able to reach that base URL — see
|
|
733
|
+
[network requirements](https://docs.kici.dev/operator/network-requirements/) and
|
|
734
|
+
[signing keys](https://docs.kici.dev/operator/orchestrator/signing-keys/) for the operator side.
|
|
735
|
+
|
|
736
|
+
## Requesting an identity token
|
|
737
|
+
|
|
738
|
+
Call `ctx.kici.oidc.token({ audience })` from a step when you need the identity
|
|
739
|
+
token for a tool that trusts your orchestrator:
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
step('mint', async (ctx) => {
|
|
743
|
+
const minted = await ctx.kici.oidc.token({ audience: 'sigstore' });
|
|
744
|
+
if ('deferred' in minted) throw new Error(`ID token deferred: ${minted.code}`);
|
|
745
|
+
const { token, expiresIn } = minted;
|
|
746
|
+
ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
|
|
747
|
+
// Hand `token` to a tool that exchanges it with a service trusting the issuer.
|
|
748
|
+
});
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
The result is either the minted token or `{ deferred: true, code }` when the
|
|
752
|
+
orchestrator could not mint one right now (`unavailable` or `failed`). An
|
|
753
|
+
exchange needs a live token, so a step that exchanges it fails on `deferred`;
|
|
754
|
+
[`ctx.attestProvenance`](https://docs.kici.dev/user/provenance/) handles that case for you by freezing
|
|
755
|
+
the statement and fulfilling it later. The token is a short-lived (about 10
|
|
756
|
+
minutes) signed JWT scoped to the current run and job. Its identity claims
|
|
757
|
+
(`repository`, `ref`, `sha`, `kici_run_id`, `kici_job_id`) are derived by the
|
|
758
|
+
orchestrator from the run context, so a step cannot spoof them. The returned
|
|
759
|
+
token value is automatically masked in step logs, and the step never holds
|
|
760
|
+
signing credentials — the orchestrator mints and signs the token on the step's
|
|
761
|
+
behalf from its own run records. It is only available inside a running job step.
|
|
762
|
+
|
|
763
|
+
## Exchanging the token with an external service
|
|
764
|
+
|
|
765
|
+
Any service that federates with a generic OIDC issuer can trust your
|
|
766
|
+
orchestrator directly. The service fetches your issuer's discovery document
|
|
767
|
+
(`<issuer>/.well-known/openid-configuration`) and public keys, so the issuer
|
|
768
|
+
base URL must be reachable from that service.
|
|
769
|
+
|
|
770
|
+
The pattern is the same everywhere: request a token with the audience the
|
|
771
|
+
service expects, exchange it for the service's own short-lived credential, and
|
|
772
|
+
use that credential inside the step. The workflows below are the ones KiCI's
|
|
773
|
+
own test suite runs against a real Cloudsmith organization and a real AWS
|
|
774
|
+
account. Each publishes a probe, reads it back, and deletes it. Replace the
|
|
775
|
+
probe with your real publish or upload.
|
|
776
|
+
|
|
777
|
+
Only the ID token is masked in step logs automatically. A credential you
|
|
778
|
+
exchange it for is an ordinary string, so do not log it. Hand it to child
|
|
779
|
+
processes through a scratch file or the environment, never on a command line.
|
|
780
|
+
|
|
781
|
+
### Cloudsmith
|
|
782
|
+
|
|
783
|
+
Cloudsmith exchanges any trusted issuer's token for a Cloudsmith API token
|
|
784
|
+
through `POST https://api.cloudsmith.io/openid/<org>/`. The exchanged
|
|
785
|
+
credential works as the Cloudsmith API key and as an npm auth token:
|
|
786
|
+
|
|
787
|
+
<!-- BEGIN GENERATED: oidc-cloudsmith-workflow (do not edit; run the doc generator) -->
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
import { writeFile } from 'node:fs/promises';
|
|
791
|
+
import { join } from 'node:path';
|
|
792
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
793
|
+
|
|
794
|
+
// Exchanges the job's OIDC ID token with Cloudsmith, publishes a probe npm
|
|
795
|
+
// package, reads it back through the Cloudsmith API and deletes it. Every
|
|
796
|
+
// Cloudsmith call is made with the exchanged token; nothing here is a secret
|
|
797
|
+
// the repo stores.
|
|
798
|
+
const ORG = 'my-org';
|
|
799
|
+
const REPO = 'my-repo';
|
|
800
|
+
const REGISTRY = `https://npm.cloudsmith.io/${ORG}/${REPO}/`;
|
|
801
|
+
const PACKAGES_API = `https://api.cloudsmith.io/v1/packages/${ORG}/${REPO}/`;
|
|
802
|
+
const PREFIX = '[cloudsmith-oidc]';
|
|
803
|
+
|
|
804
|
+
function decodeJwtPart(token: string, index: number): Record<string, unknown> {
|
|
805
|
+
return JSON.parse(Buffer.from(token.split('.')[index], 'base64url').toString('utf8'));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export default workflow('publish', {
|
|
809
|
+
on: push({ branches: ['main'] }),
|
|
810
|
+
jobs: [
|
|
811
|
+
job('publish', {
|
|
812
|
+
runsOn: 'container',
|
|
813
|
+
steps: [
|
|
814
|
+
step('exchange-and-publish', async (ctx) => {
|
|
815
|
+
// 1. Mint. Log the signing algorithm, key id and subject: when the
|
|
816
|
+
// provider rejects the token, this line says which claim to compare
|
|
817
|
+
// against the provider's required claims.
|
|
818
|
+
const minted = await ctx.kici.oidc.token({ audience: 'cloudsmith' });
|
|
819
|
+
// A deferred mint is a transient orchestrator-side failure; an exchange
|
|
820
|
+
// needs a live token, so the step fails rather than freezing a statement.
|
|
821
|
+
if ('deferred' in minted) throw new Error(`ID token mint deferred: ${minted.code}`);
|
|
822
|
+
const { token } = minted;
|
|
823
|
+
const header = decodeJwtPart(token, 0);
|
|
824
|
+
const payload = decodeJwtPart(token, 1);
|
|
825
|
+
ctx.log.info(`${PREFIX} alg=${header.alg} kid=${header.kid} sub=${payload.sub}`);
|
|
826
|
+
|
|
827
|
+
// 2. Exchange.
|
|
828
|
+
const exchange = await fetch(`https://api.cloudsmith.io/openid/${ORG}/`, {
|
|
829
|
+
method: 'POST',
|
|
830
|
+
headers: { 'content-type': 'application/json' },
|
|
831
|
+
body: JSON.stringify({ oidc_token: token, service_slug: 'ci-publisher' }),
|
|
832
|
+
});
|
|
833
|
+
if (!exchange.ok) {
|
|
834
|
+
const body = (await exchange.text()).split(token).join('<id-token>');
|
|
835
|
+
ctx.log.error(`${PREFIX} exchange=failed status=${exchange.status} body=${body}`);
|
|
836
|
+
throw new Error(`Cloudsmith token exchange failed: ${exchange.status}`);
|
|
837
|
+
}
|
|
838
|
+
const { token: csToken } = (await exchange.json()) as { token: string };
|
|
839
|
+
ctx.log.info(`${PREFIX} exchange=ok`);
|
|
840
|
+
|
|
841
|
+
// 3. Publish a unique probe version. The npmrc carrying the exchanged
|
|
842
|
+
// token is a scratch file the job removes; it never reaches argv.
|
|
843
|
+
// The run-id segment is prefixed with a letter: a prerelease identifier
|
|
844
|
+
// made only of digits is numeric under semver and may not start with
|
|
845
|
+
// 0, so a run id such as 01234567 would make npm refuse the version.
|
|
846
|
+
const runIdShort = String(payload.kici_run_id).slice(0, 8);
|
|
847
|
+
const version = `0.0.0-e2e.r${runIdShort}.${Math.floor(Date.now() / 1000)}`;
|
|
848
|
+
const name = '@kici-e2e/oidc-probe';
|
|
849
|
+
const pkgDir = await ctx.mktemp('oidc-probe');
|
|
850
|
+
await writeFile(
|
|
851
|
+
join(pkgDir.path, 'package.json'),
|
|
852
|
+
JSON.stringify({ name, version, license: 'MIT', main: 'index.js' }, null, 2),
|
|
853
|
+
);
|
|
854
|
+
await writeFile(join(pkgDir.path, 'index.js'), 'module.exports = "kici oidc probe";\n');
|
|
855
|
+
const npmrc = await ctx.mktempFile('cloudsmith-npmrc');
|
|
856
|
+
await writeFile(
|
|
857
|
+
npmrc.path,
|
|
858
|
+
`//npm.cloudsmith.io/${ORG}/${REPO}/:_authToken=${csToken}\n`,
|
|
859
|
+
);
|
|
860
|
+
await ctx.$({
|
|
861
|
+
cwd: pkgDir.path,
|
|
862
|
+
env: { ...process.env, NPM_CONFIG_USERCONFIG: npmrc.path },
|
|
863
|
+
})`npm publish --registry ${REGISTRY} --tag e2e`;
|
|
864
|
+
|
|
865
|
+
// 4. Read it back through the API with the same token, then delete.
|
|
866
|
+
// The version is unique per run, so it is the whole query.
|
|
867
|
+
const authHeaders = { 'X-Api-Key': `Bearer ${csToken}` };
|
|
868
|
+
const query = encodeURIComponent(`version:${version}`);
|
|
869
|
+
let slug: string | undefined;
|
|
870
|
+
const deadline = Date.now() + 120_000;
|
|
871
|
+
while (Date.now() < deadline && !slug) {
|
|
872
|
+
const list = await fetch(`${PACKAGES_API}?query=${query}`, { headers: authHeaders });
|
|
873
|
+
if (list.ok) {
|
|
874
|
+
const rows = (await list.json()) as Array<{
|
|
875
|
+
slug: string;
|
|
876
|
+
is_sync_completed: boolean;
|
|
877
|
+
}>;
|
|
878
|
+
const done = rows.find((r) => r.is_sync_completed);
|
|
879
|
+
if (done) slug = done.slug;
|
|
880
|
+
}
|
|
881
|
+
if (!slug) await new Promise((r) => setTimeout(r, 5_000));
|
|
882
|
+
}
|
|
883
|
+
if (!slug) throw new Error(`${name}@${version} never finished syncing in Cloudsmith`);
|
|
884
|
+
const del = await fetch(`${PACKAGES_API}${slug}/`, {
|
|
885
|
+
method: 'DELETE',
|
|
886
|
+
headers: authHeaders,
|
|
887
|
+
});
|
|
888
|
+
if (!del.ok) throw new Error(`Cloudsmith delete failed: ${del.status}`);
|
|
889
|
+
ctx.log.info(`${PREFIX} verdict=ok package=${name}@${version} deleted=true`);
|
|
890
|
+
}),
|
|
891
|
+
],
|
|
892
|
+
}),
|
|
893
|
+
],
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
<!-- END GENERATED: oidc-cloudsmith-workflow -->
|
|
898
|
+
|
|
899
|
+
On the Cloudsmith side, add an OpenID Connect provider under your organization's
|
|
900
|
+
authentication settings. The **provider URL** is your orchestrator's issuer
|
|
901
|
+
(`KICI_ORCHESTRATOR_PROVENANCE_ISSUER`), and the **service account** is the one
|
|
902
|
+
named by `service_slug`. The **required claims** pin which runs may
|
|
903
|
+
authenticate. Pin `sub` to the build identity; for production add the fork
|
|
904
|
+
conditions from the [claim table](https://docs.kici.dev/user/oidc/#the-claim-set) below so a fork pull request
|
|
905
|
+
cannot exchange a token:
|
|
906
|
+
|
|
907
|
+
```
|
|
908
|
+
sub: repo:acme/app:ref:main:workflow:publish
|
|
909
|
+
is_fork: false
|
|
910
|
+
head_repository: acme/app
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### AWS
|
|
914
|
+
|
|
915
|
+
AWS exchanges the token through `AssumeRoleWithWebIdentity`, which needs no AWS
|
|
916
|
+
credential of its own. The step imports `@aws-sdk/client-sts` and
|
|
917
|
+
`@aws-sdk/client-s3`; declare both under `dependencies` in `.kici/package.json`
|
|
918
|
+
so the compiled workflow can load them.
|
|
919
|
+
|
|
920
|
+
<!-- BEGIN GENERATED: oidc-aws-workflow (do not edit; run the doc generator) -->
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
924
|
+
import { STSClient, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
|
|
925
|
+
import {
|
|
926
|
+
S3Client,
|
|
927
|
+
PutObjectCommand,
|
|
928
|
+
GetObjectCommand,
|
|
929
|
+
DeleteObjectCommand,
|
|
930
|
+
ListObjectsV2Command,
|
|
931
|
+
} from '@aws-sdk/client-s3';
|
|
932
|
+
|
|
933
|
+
// Exchanges the job's OIDC ID token with AWS STS for role credentials, writes
|
|
934
|
+
// one probe object to S3, reads it back and deletes it. Nothing here is a
|
|
935
|
+
// secret the repo stores; the credentials exist only inside this step.
|
|
936
|
+
const REGION = 'eu-central-1';
|
|
937
|
+
const ROLE_ARN = 'arn:aws:iam::123456789012:role/ci-uploader';
|
|
938
|
+
const BUCKET = 'my-artifacts';
|
|
939
|
+
const PREFIX = '[aws-oidc]';
|
|
940
|
+
|
|
941
|
+
function decodeJwtPart(token: string, index: number): Record<string, unknown> {
|
|
942
|
+
return JSON.parse(Buffer.from(token.split('.')[index], 'base64url').toString('utf8'));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export default workflow('upload', {
|
|
946
|
+
on: push({ branches: ['main'] }),
|
|
947
|
+
jobs: [
|
|
948
|
+
job('upload', {
|
|
949
|
+
runsOn: 'container',
|
|
950
|
+
steps: [
|
|
951
|
+
step('assume-and-put', async (ctx) => {
|
|
952
|
+
// 1. Mint. Log the signing algorithm, key id, subject and audience:
|
|
953
|
+
// when STS rejects the token, this line says which claim to compare
|
|
954
|
+
// against the role's trust policy.
|
|
955
|
+
const minted = await ctx.kici.oidc.token({ audience: 'sts.amazonaws.com' });
|
|
956
|
+
// A deferred mint is a transient orchestrator-side failure; an exchange
|
|
957
|
+
// needs a live token, so the step fails rather than freezing a statement.
|
|
958
|
+
if ('deferred' in minted) throw new Error(`ID token mint deferred: ${minted.code}`);
|
|
959
|
+
const { token } = minted;
|
|
960
|
+
const header = decodeJwtPart(token, 0);
|
|
961
|
+
const payload = decodeJwtPart(token, 1);
|
|
962
|
+
ctx.log.info(
|
|
963
|
+
`${PREFIX} alg=${header.alg} kid=${header.kid} sub=${payload.sub} aud=${payload.aud}`,
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
// 2. Exchange. AssumeRoleWithWebIdentity is an unsigned operation, so
|
|
967
|
+
// the client needs no credentials of its own.
|
|
968
|
+
const runId = String(payload.kici_run_id);
|
|
969
|
+
const sts = new STSClient({ region: REGION });
|
|
970
|
+
let creds;
|
|
971
|
+
try {
|
|
972
|
+
const out = await sts.send(
|
|
973
|
+
new AssumeRoleWithWebIdentityCommand({
|
|
974
|
+
RoleArn: ROLE_ARN,
|
|
975
|
+
RoleSessionName: `kici-e2e-${runId.slice(0, 8)}`,
|
|
976
|
+
WebIdentityToken: token,
|
|
977
|
+
DurationSeconds: 900,
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
creds = out.Credentials;
|
|
981
|
+
} catch (err) {
|
|
982
|
+
const e = err as { name?: string; message?: string };
|
|
983
|
+
const message = String(e.message ?? err)
|
|
984
|
+
.split(token)
|
|
985
|
+
.join('<id-token>');
|
|
986
|
+
ctx.log.error(`${PREFIX} assume=failed code=${e.name ?? 'unknown'} message=${message}`);
|
|
987
|
+
throw err;
|
|
988
|
+
}
|
|
989
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
|
|
990
|
+
throw new Error('AssumeRoleWithWebIdentity returned no credentials');
|
|
991
|
+
}
|
|
992
|
+
ctx.log.info(`${PREFIX} assume=ok`);
|
|
993
|
+
|
|
994
|
+
// 3. Put + get with the role credentials.
|
|
995
|
+
const s3 = new S3Client({
|
|
996
|
+
region: REGION,
|
|
997
|
+
credentials: {
|
|
998
|
+
accessKeyId: creds.AccessKeyId,
|
|
999
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
1000
|
+
sessionToken: creds.SessionToken,
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
const key = `e2e/${runId}/probe.txt`;
|
|
1004
|
+
const body = `kici aws-oidc probe run=${runId} ts=${new Date().toISOString()}\n`;
|
|
1005
|
+
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body }));
|
|
1006
|
+
const got = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
|
|
1007
|
+
const readBack = await got.Body?.transformToString();
|
|
1008
|
+
if (readBack !== body) {
|
|
1009
|
+
throw new Error(`probe object did not round-trip: got ${JSON.stringify(readBack)}`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 4. Delete, then prove the prefix is empty.
|
|
1013
|
+
await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key }));
|
|
1014
|
+
const left = await s3.send(
|
|
1015
|
+
new ListObjectsV2Command({ Bucket: BUCKET, Prefix: `e2e/${runId}/` }),
|
|
1016
|
+
);
|
|
1017
|
+
if ((left.KeyCount ?? 0) !== 0) {
|
|
1018
|
+
throw new Error(`${left.KeyCount} object(s) left under e2e/${runId}/`);
|
|
1019
|
+
}
|
|
1020
|
+
ctx.log.info(`${PREFIX} verdict=ok bucket=${BUCKET} key=${key} deleted=true`);
|
|
1021
|
+
}),
|
|
1022
|
+
],
|
|
1023
|
+
}),
|
|
1024
|
+
],
|
|
1025
|
+
});
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
<!-- END GENERATED: oidc-aws-workflow -->
|
|
1029
|
+
|
|
1030
|
+
On the AWS side, create an IAM OpenID Connect identity provider whose URL is
|
|
1031
|
+
your orchestrator's issuer and whose audience is `sts.amazonaws.com`, then
|
|
1032
|
+
attach the [worked trust policy](https://docs.kici.dev/user/oidc/#a-worked-aws-trust-policy) to the role. When
|
|
1033
|
+
the issuer URL carries a path, the condition keys carry it too:
|
|
1034
|
+
`orch.example.com/kici:sub`, not `orch.example.com:sub`.
|
|
1035
|
+
|
|
1036
|
+
## ID-token claims and cloud trust policies
|
|
1037
|
+
|
|
1038
|
+
A cloud provider's OIDC trust policy decides which builds may assume a role. The
|
|
1039
|
+
token below is what your policy matches on, so read this section before you
|
|
1040
|
+
write one.
|
|
1041
|
+
|
|
1042
|
+
### The claim set
|
|
1043
|
+
|
|
1044
|
+
| Claim | Value |
|
|
1045
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
1046
|
+
| `iss` | Your orchestrator's provenance issuer |
|
|
1047
|
+
| `aud` | The audience you asked for |
|
|
1048
|
+
| `sub` | The build identity — see the two shapes below |
|
|
1049
|
+
| `repository` | `owner/repo` the run acted on |
|
|
1050
|
+
| `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
|
|
1051
|
+
| `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
|
|
1052
|
+
| `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
|
|
1053
|
+
| `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
|
|
1054
|
+
| `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
|
|
1055
|
+
| `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
|
|
1056
|
+
| `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
|
|
1057
|
+
| `actor` | Provider login of the triggering actor |
|
|
1058
|
+
| `sha` | The run's commit |
|
|
1059
|
+
| `workflow_ref` | `<workflow name>@<sha>` |
|
|
1060
|
+
| `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
|
|
1061
|
+
| `org_id` | Your organization id |
|
|
1062
|
+
| `orchestrator_id` | The orchestrator that ran the job |
|
|
1063
|
+
| `provider` | The source provider the run came from (`github`, `gitlab`, …) |
|
|
1064
|
+
| `source_origin` | `triggered` for a webhook-driven run; `run-remote` when the run executed an uploaded working tree (`kici run remote`) |
|
|
1065
|
+
| `attestation_origin` | `live` when the token was minted during the job; `deferred` or `offline-backfill` when it was minted later for a frozen statement |
|
|
1066
|
+
| `statement_hash` | The hash of the frozen statement a deferred token is bound to; `null` for a live token |
|
|
1067
|
+
|
|
1068
|
+
Every claim in the table is **always present**. A value the run did not resolve
|
|
1069
|
+
is `''`, `'unresolved'` or `null`, never omitted and never guessed. That matters: an
|
|
1070
|
+
absent claim makes a `StringEquals` condition pass, which would silently remove
|
|
1071
|
+
a constraint you wrote expecting it to be enforced.
|
|
1072
|
+
|
|
1073
|
+
### The two `sub` shapes
|
|
1074
|
+
|
|
1075
|
+
```
|
|
1076
|
+
push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
|
|
1077
|
+
pull request, review repo:<owner/repo>:pull_request
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
|
|
1081
|
+
pull request's `ref` is its base branch. So a ref-bearing subject would be
|
|
1082
|
+
identical for a fork pull request targeting `main` and a trusted push to `main`.
|
|
1083
|
+
A policy pinning that subject would hand your cloud role to any contributor who
|
|
1084
|
+
opened a pull request running the same workflow.
|
|
1085
|
+
|
|
1086
|
+
**A re-run keeps the shape of the run it repeats.** Re-running a pull-request
|
|
1087
|
+
run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
|
|
1088
|
+
commit from the same source. Its `event_name` claim still reads `rerun` — that
|
|
1089
|
+
claim says what started the run, while `sub` says which identity the run
|
|
1090
|
+
presents. A policy that pins the branch-shaped subject therefore does not match
|
|
1091
|
+
a re-run of a pull request, which is the same protection the first run gets.
|
|
1092
|
+
|
|
1093
|
+
### A worked AWS trust policy
|
|
1094
|
+
|
|
1095
|
+
Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
|
|
1096
|
+
ran; it does not tell you whose code ran.
|
|
1097
|
+
|
|
1098
|
+
```json
|
|
1099
|
+
{
|
|
1100
|
+
"Version": "2012-10-17",
|
|
1101
|
+
"Statement": [
|
|
1102
|
+
{
|
|
1103
|
+
"Effect": "Allow",
|
|
1104
|
+
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
|
|
1105
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
1106
|
+
"Condition": {
|
|
1107
|
+
"StringEquals": {
|
|
1108
|
+
"orch.example.com:aud": "sts.amazonaws.com",
|
|
1109
|
+
"orch.example.com:sub": "repo:acme/app:ref:main:workflow:upload",
|
|
1110
|
+
"orch.example.com:is_fork": "false",
|
|
1111
|
+
"orch.example.com:head_repository": "acme/app",
|
|
1112
|
+
"orch.example.com:trust_tier": "trusted"
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
}
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
This grants the role only to a run on `main` in `acme/app`, from code in that
|
|
1121
|
+
same repository, triggered by an actor your orchestrator resolved as trusted.
|
|
1122
|
+
A fork pull request fails on every one of the extra conditions, and a run whose
|
|
1123
|
+
context did not resolve fails too — `'unresolved'` matches none of them, so the
|
|
1124
|
+
policy fails closed.
|
|
1125
|
+
|
|
1126
|
+
To let a same-repo pull request assume the role, add a second statement pinning
|
|
1127
|
+
`"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
|
|
1128
|
+
`"head_repository": "acme/app"`.
|
|
1129
|
+
|
|
1130
|
+
### Migrating an existing policy
|
|
1131
|
+
|
|
1132
|
+
If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
|
|
1133
|
+
matching once you upgrade — which is the fix, because it was matching runs it
|
|
1134
|
+
should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
|
|
1135
|
+
conditions above. The same move covers a re-run of a pull request, which
|
|
1136
|
+
presents the pull-request subject too.
|
|
1137
|
+
|
|
1138
|
+
## See also
|
|
1139
|
+
|
|
1140
|
+
- [Build provenance and attestations](https://docs.kici.dev/user/provenance/) — `ctx.attestProvenance`
|
|
1141
|
+
builds on the identity token this page mints.
|
|
1142
|
+
- [Private registries](https://docs.kici.dev/user/private-registries/) — registry auth for installs,
|
|
1143
|
+
and where an exchanged Cloudsmith token fits.
|
|
1144
|
+
- [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.kici.oidc.token` step
|
|
1145
|
+
API in full.
|
|
1146
|
+
- [Signing keys](https://docs.kici.dev/operator/orchestrator/signing-keys/) — provisioning and
|
|
1147
|
+
rotating the key that signs these tokens.
|
|
1148
|
+
- [Network requirements](https://docs.kici.dev/operator/network-requirements/) — what the
|
|
1149
|
+
exchanging service must reach.
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
721
1153
|
## Private npm registries
|
|
722
1154
|
|
|
723
1155
|
Source: https://docs.kici.dev/user/private-registries/
|
|
@@ -941,6 +1373,10 @@ registries: [
|
|
|
941
1373
|
],
|
|
942
1374
|
```
|
|
943
1375
|
|
|
1376
|
+
To publish to Cloudsmith from a step without a stored token, exchange the job's
|
|
1377
|
+
OIDC identity token for a Cloudsmith credential — see
|
|
1378
|
+
[exchanging the token with an external service](https://docs.kici.dev/user/oidc/#exchanging-the-token-with-an-external-service).
|
|
1379
|
+
|
|
944
1380
|
## Security model
|
|
945
1381
|
|
|
946
1382
|
- **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.
|
|
@@ -972,7 +1408,7 @@ The orchestrator exposes Prometheus counters and a histogram under the `kici_orc
|
|
|
972
1408
|
| ------------------------------------------------------------- | --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
973
1409
|
| `kici_orch_install_secrets_decisions_total` | Counter | `decision`, `reason` | Pass / reject / hold volume. `decision=hold` (reason `held`) counts dispatches paused at a reviewer-gated install context. Reject reasons enumerate the failure mode: `malformed_ref`, `invalid_url_scheme`, `env_not_found`, `protection_rule_block`, `missing_token`, `missing_install_env`, etc. |
|
|
974
1410
|
| `kici_orch_install_secrets_npm_registry_used_total` | Counter | `channel`, `provider`, `scope` | Per-channel + per-scope usage. `channel=registries` is Option A, `channel=install_env` is Option C. `scope=default` marks a no-scope default registry; `scope=-` marks Option C entries. |
|
|
975
|
-
| `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs
|
|
1411
|
+
| `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs). Expected to be 0 in single-tenant orgs. |
|
|
976
1412
|
| `kici_orch_install_secrets_token_resolution_duration_seconds` | Histogram | `environment` | Latency of per-environment secret resolution. Pathological tails (>500ms) usually mean a Vault timeout or a slow Postgres replica. |
|
|
977
1413
|
|
|
978
1414
|
The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-secrets.json`; if you maintain your own monitoring stack, you can import it directly.
|
|
@@ -1069,7 +1505,10 @@ path or a precomputed digest:
|
|
|
1069
1505
|
|
|
1070
1506
|
The identity token is fetched and masked in logs automatically — you never
|
|
1071
1507
|
handle it. The call returns `{ storageKey, subjectDigest, bundleMediaType }`
|
|
1072
|
-
identifying the stored bundle.
|
|
1508
|
+
identifying the stored bundle. `ctx.attestProvenance` builds on the run's OIDC
|
|
1509
|
+
identity token; to mint that token yourself and exchange it with Cloudsmith,
|
|
1510
|
+
AWS, or any OIDC-federated service, see
|
|
1511
|
+
[Workload identity with OIDC](https://docs.kici.dev/user/oidc/).
|
|
1073
1512
|
|
|
1074
1513
|
`ctx.attestProvenance` is only available inside a running job step; calling it
|
|
1075
1514
|
outside one rejects with a clear error. `kici run --local` runs are supported:
|
|
@@ -1077,128 +1516,6 @@ the offline local dev plane signs with a dev identity under the
|
|
|
1077
1516
|
clearly-non-production issuer `kici-local`, and those bundles verify against a
|
|
1078
1517
|
trust root exported with `kici local trust-root`.
|
|
1079
1518
|
|
|
1080
|
-
### Requesting a raw identity token
|
|
1081
|
-
|
|
1082
|
-
`ctx.attestProvenance` builds on a lower-level primitive you can call directly
|
|
1083
|
-
when you need the identity token for a different tool:
|
|
1084
|
-
|
|
1085
|
-
```typescript
|
|
1086
|
-
step('mint', async (ctx) => {
|
|
1087
|
-
const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
|
|
1088
|
-
ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
|
|
1089
|
-
// Hand `token` to a tool that exchanges it with a service trusting the issuer.
|
|
1090
|
-
});
|
|
1091
|
-
```
|
|
1092
|
-
|
|
1093
|
-
The token is a short-lived (about 10 minutes) signed JWT scoped to the current
|
|
1094
|
-
run and job. Its identity claims (`repository`, `ref`, `sha`, `kici_run_id`,
|
|
1095
|
-
`kici_job_id`) are derived by the orchestrator from the run context, so a step
|
|
1096
|
-
cannot spoof them. The returned token value is automatically masked in step logs,
|
|
1097
|
-
and the step never holds signing credentials — the orchestrator mints and signs
|
|
1098
|
-
the token on the step's behalf from its own run records. Like `attestProvenance`,
|
|
1099
|
-
it is only available inside a running job step.
|
|
1100
|
-
|
|
1101
|
-
## ID-token claims and cloud trust policies
|
|
1102
|
-
|
|
1103
|
-
A cloud provider's OIDC trust policy decides which builds may assume a role. The
|
|
1104
|
-
token below is what your policy matches on, so read this section before you
|
|
1105
|
-
write one.
|
|
1106
|
-
|
|
1107
|
-
### The claim set
|
|
1108
|
-
|
|
1109
|
-
| Claim | Value |
|
|
1110
|
-
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
1111
|
-
| `iss` | Your orchestrator's provenance issuer |
|
|
1112
|
-
| `aud` | The audience you asked for |
|
|
1113
|
-
| `sub` | The build identity — see the two shapes below |
|
|
1114
|
-
| `repository` | `owner/repo` the run acted on |
|
|
1115
|
-
| `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
|
|
1116
|
-
| `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
|
|
1117
|
-
| `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
|
|
1118
|
-
| `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
|
|
1119
|
-
| `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
|
|
1120
|
-
| `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
|
|
1121
|
-
| `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
|
|
1122
|
-
| `actor` | Provider login of the triggering actor |
|
|
1123
|
-
| `sha` | The run's commit |
|
|
1124
|
-
| `workflow_ref` | `<workflow name>@<sha>` |
|
|
1125
|
-
| `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
|
|
1126
|
-
| `org_id` | Your organization id |
|
|
1127
|
-
|
|
1128
|
-
Every claim in the table is **always present**. A value the run did not resolve
|
|
1129
|
-
is `''` or `'unresolved'`, never omitted and never guessed. That matters: an
|
|
1130
|
-
absent claim makes a `StringEquals` condition pass, which would silently remove
|
|
1131
|
-
a constraint you wrote expecting it to be enforced.
|
|
1132
|
-
|
|
1133
|
-
### The two `sub` shapes
|
|
1134
|
-
|
|
1135
|
-
```
|
|
1136
|
-
push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
|
|
1137
|
-
pull request, review repo:<owner/repo>:pull_request
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
|
|
1141
|
-
pull request's `ref` is its base branch. So a ref-bearing subject would be
|
|
1142
|
-
identical for a fork pull request targeting `main` and a trusted push to `main`.
|
|
1143
|
-
A policy pinning that subject would hand your cloud role to any contributor who
|
|
1144
|
-
opened a pull request running the same workflow.
|
|
1145
|
-
|
|
1146
|
-
**A re-run keeps the shape of the run it repeats.** Re-running a pull-request
|
|
1147
|
-
run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
|
|
1148
|
-
commit from the same source. Its `event_name` claim still reads `rerun` — that
|
|
1149
|
-
claim says what started the run, while `sub` says which identity the run
|
|
1150
|
-
presents. A policy that pins the branch-shaped subject therefore does not match
|
|
1151
|
-
a re-run of a pull request, which is the same protection the first run gets.
|
|
1152
|
-
|
|
1153
|
-
### A worked AWS trust policy
|
|
1154
|
-
|
|
1155
|
-
Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
|
|
1156
|
-
ran; it does not tell you whose code ran.
|
|
1157
|
-
|
|
1158
|
-
```json
|
|
1159
|
-
{
|
|
1160
|
-
"Version": "2012-10-17",
|
|
1161
|
-
"Statement": [
|
|
1162
|
-
{
|
|
1163
|
-
"Effect": "Allow",
|
|
1164
|
-
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
|
|
1165
|
-
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
1166
|
-
"Condition": {
|
|
1167
|
-
"StringEquals": {
|
|
1168
|
-
"orch.example.com:aud": "sts.amazonaws.com",
|
|
1169
|
-
"orch.example.com:sub": "repo:acme/app:ref:main:workflow:deploy",
|
|
1170
|
-
"orch.example.com:is_fork": "false",
|
|
1171
|
-
"orch.example.com:head_repository": "acme/app",
|
|
1172
|
-
"orch.example.com:trust_tier": "trusted"
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
]
|
|
1177
|
-
}
|
|
1178
|
-
```
|
|
1179
|
-
|
|
1180
|
-
This grants the role only to a run on `main` in `acme/app`, from code in that
|
|
1181
|
-
same repository, triggered by an actor your orchestrator resolved as trusted.
|
|
1182
|
-
A fork pull request fails on all three of the extra conditions, and a run whose
|
|
1183
|
-
context did not resolve fails too — `'unresolved'` matches none of them, so the
|
|
1184
|
-
policy fails closed.
|
|
1185
|
-
|
|
1186
|
-
To let a same-repo pull request assume the role, add a second statement pinning
|
|
1187
|
-
`"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
|
|
1188
|
-
`"head_repository": "acme/app"`.
|
|
1189
|
-
|
|
1190
|
-
### Migrating an existing policy
|
|
1191
|
-
|
|
1192
|
-
If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
|
|
1193
|
-
matching once you upgrade — which is the fix, because it was matching runs it
|
|
1194
|
-
should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
|
|
1195
|
-
conditions above. The same move covers a re-run of a pull request, which
|
|
1196
|
-
presents the pull-request subject too.
|
|
1197
|
-
|
|
1198
|
-
While you migrate, `KICI_OIDC_LEGACY_PR_SUB=1` on the orchestrator restores the
|
|
1199
|
-
old subject. It restores the collision with it, so treat it as a short bridge,
|
|
1200
|
-
not a setting. See [deprecations](https://docs.kici.dev/user/deprecations/).
|
|
1201
|
-
|
|
1202
1519
|
## Verifying an attestation
|
|
1203
1520
|
|
|
1204
1521
|
Verify a bundle with the `kici verify-attestation` command. It establishes the
|
|
@@ -1374,8 +1691,10 @@ This page shows the parsed provenance for one attestation.
|
|
|
1374
1691
|
|
|
1375
1692
|
## See also
|
|
1376
1693
|
|
|
1377
|
-
- [
|
|
1378
|
-
|
|
1694
|
+
- [Workload identity with OIDC](https://docs.kici.dev/user/oidc/) — minting the identity token
|
|
1695
|
+
yourself and exchanging it with an external service.
|
|
1696
|
+
- [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.attestProvenance` step
|
|
1697
|
+
API in full.
|
|
1379
1698
|
- [CLI reference](https://docs.kici.dev/user/cli/notifications-and-diagnostics/#kici-verify-attestation) — every
|
|
1380
1699
|
`kici verify-attestation` flag and exit code.
|
|
1381
1700
|
|