@kici-dev/sdk 0.5.0 → 0.6.1

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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @deprecated Pass a claim code (ClaimCodeCredentials) so the token never
3
+ * transits cloud-init. Removed at v1.0.0.
4
+ */
5
+ export interface CloudInitCredentials {
6
+ agentToken: string;
7
+ agentId: string;
8
+ orchestratorUrl: string;
9
+ labels: string[];
10
+ }
11
+ /** Preferred: the agent self-claims from a single-use code; the token is minted in-instance. */
12
+ export interface ClaimCodeCredentials {
13
+ claimCode: string;
14
+ agentId: string;
15
+ orchestratorUrl: string;
16
+ labels: string[];
17
+ }
18
+ /** Either credential shape accepted by `buildAgentCloudInit`. */
19
+ export type AgentCloudInitCredentials = CloudInitCredentials | ClaimCodeCredentials;
20
+ /** How the agent binary is delivered onto the instance. */
21
+ export type AgentDeliveryMode = 'container' | 'payload';
22
+ /** How the rendered `user_data` string is encoded before it is returned. */
23
+ export type UserDataEncoding = 'raw' | 'base64';
24
+ /** A cloud-init `write_files` entry the caller adds. */
25
+ export interface CloudInitWriteFile {
26
+ path: string;
27
+ content: string;
28
+ permissions?: string;
29
+ owner?: string;
30
+ }
31
+ export interface AgentCloudInitOptions {
32
+ /** Hard lifetime cap (minutes) after which the instance powers itself off (L2). */
33
+ maxLifetimeMinutes: number;
34
+ /** 'container' (docker run the published agent image) | 'payload' (fetch from orchestrator). */
35
+ deliveryMode?: AgentDeliveryMode;
36
+ /** Container image ref (container mode). */
37
+ agentImage?: string;
38
+ /** Escape hatch: fully override the agent-start command (ignores deliveryMode/agentImage). */
39
+ startCommand?: string;
40
+ /** apt/yum packages → cloud-init `packages:` (unioned with the base). */
41
+ packages?: string[];
42
+ /** Extra `write_files` entries (the reserved env-file path is rejected). */
43
+ writeFiles?: CloudInitWriteFile[];
44
+ /** runcmd lines injected BEFORE the agent starts. */
45
+ runcmdBefore?: string[];
46
+ /** runcmd lines injected AFTER the agent starts. */
47
+ runcmdAfter?: string[];
48
+ /** Extra env appended to the agent env file (keys validated, newline values rejected). */
49
+ agentEnv?: Record<string, string>;
50
+ /** Raw cloud-config YAML to merge everything into (users, ssh, apt, mounts, bootcmd, …). */
51
+ baseCloudConfig?: string;
52
+ /**
53
+ * Encoding of the returned `user_data` string. `'raw'` (the default) returns
54
+ * the plain `#cloud-config` text — what Hetzner `user_data` expects. `'base64'`
55
+ * returns the same text base64-encoded, the form AWS EC2 `UserData` and Azure
56
+ * `customData` expect.
57
+ */
58
+ userDataEncoding?: UserDataEncoding;
59
+ }
60
+ /**
61
+ * Build the cloud-config `user_data` string. In the token form the agent token
62
+ * appears only in the reserved env-file write entry (`0600`, root-only); the
63
+ * claim-code form keeps the token off the provisioning channel entirely.
64
+ */
65
+ export declare function buildAgentCloudInit(creds: AgentCloudInitCredentials, options: AgentCloudInitOptions): string;
66
+ //# sourceMappingURL=agent-cloud-init.d.ts.map
@@ -0,0 +1,128 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ import { parse, stringify } from "yaml";
3
+ //#region src/agent-cloud-init.ts
4
+ /**
5
+ * Cloud-init `user_data` builder for a scaler-provisioned KiCI agent.
6
+ *
7
+ * Renders a `#cloud-config` that installs + starts the agent with the claimed
8
+ * ephemeral credentials, plus teardown layer L2 — an in-instance max-lifetime
9
+ * self-poweroff. The agent token is written ONLY into a root-only `0600` env
10
+ * file; it never appears in a comment, a process argument, or any other file.
11
+ */
12
+ const AGENT_ENV_FILE = "/etc/kici-agent.env";
13
+ const DEFAULT_AGENT_IMAGE = "quay.io/kici-dev/kici-agent:latest";
14
+ /**
15
+ * The credential-specific env lines. The claim-code form emits a single-use
16
+ * code the agent exchanges for its own token in-instance; the token form emits
17
+ * the token directly (the ONLY place the token appears).
18
+ */
19
+ function credentialEnvLines(creds) {
20
+ if ("claimCode" in creds) return [
21
+ `KICI_ORCHESTRATOR_URL=${creds.orchestratorUrl}`,
22
+ `KICI_SCALER_CLAIM_CODE=${creds.claimCode}`,
23
+ `KICI_AGENT_ID=${creds.agentId}`,
24
+ `KICI_LABELS=${creds.labels.join(",")}`
25
+ ];
26
+ return [
27
+ `KICI_ORCHESTRATOR_URL=${creds.orchestratorUrl}`,
28
+ `KICI_AGENT_TOKEN=${creds.agentToken}`,
29
+ `KICI_AGENT_ID=${creds.agentId}`,
30
+ `KICI_LABELS=${creds.labels.join(",")}`
31
+ ];
32
+ }
33
+ /** Render the agent env-file content (the ONLY place the token appears, in the token form). */
34
+ function renderEnvFileContent(creds, agentEnv) {
35
+ return `${[
36
+ ...credentialEnvLines(creds),
37
+ "KICI_SCALER_MANAGED=1",
38
+ ...Object.entries(agentEnv ?? {}).map(([k, v]) => `${k}=${v}`)
39
+ ].join("\n")}\n`;
40
+ }
41
+ /** Dedupe a list preserving first-seen order. */
42
+ function dedupe(items) {
43
+ return [...new Set(items)];
44
+ }
45
+ /** POSIX env-name shape: a letter or underscore, then letters/digits/underscores. */
46
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
47
+ /** Reject agentEnv keys that are not POSIX env names and values carrying a newline. */
48
+ function validateAgentEnv(agentEnv) {
49
+ for (const [key, value] of Object.entries(agentEnv ?? {})) {
50
+ if (!ENV_NAME_RE.test(key)) throw new Error(`buildAgentCloudInit: invalid agentEnv key "${key}" (must be a POSIX env name)`);
51
+ if (value.includes("\n")) throw new Error(`buildAgentCloudInit: agentEnv value for "${key}" must not contain a newline`);
52
+ }
53
+ }
54
+ /** Reject any caller/base write_files entry targeting the reserved env-file path. */
55
+ function assertNoReservedWrite(writeFiles, where) {
56
+ for (const wf of writeFiles ?? []) if (wf.path === AGENT_ENV_FILE) throw new Error(`buildAgentCloudInit: ${where} may not write the reserved path ${AGENT_ENV_FILE}`);
57
+ }
58
+ /** Parse + shape-check a caller-supplied base cloud-config. */
59
+ function parseBase(baseCloudConfig) {
60
+ let parsed;
61
+ try {
62
+ parsed = parse(baseCloudConfig);
63
+ } catch (err) {
64
+ throw new Error(`buildAgentCloudInit: baseCloudConfig is not valid YAML: ${err.message}`);
65
+ }
66
+ if (parsed == null) return {};
67
+ if (typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("buildAgentCloudInit: baseCloudConfig must be a cloud-config mapping");
68
+ const base = parsed;
69
+ for (const key of [
70
+ "packages",
71
+ "runcmd",
72
+ "write_files"
73
+ ]) if (base[key] !== void 0 && !Array.isArray(base[key])) throw new Error(`buildAgentCloudInit: baseCloudConfig.${key} must be a list`);
74
+ return base;
75
+ }
76
+ /** Render the command that starts the agent for the chosen delivery mode. */
77
+ function renderStartCommand(opts) {
78
+ if (opts.startCommand) return opts.startCommand;
79
+ if (opts.deliveryMode === "payload") return "/usr/local/bin/kici-agent-bootstrap";
80
+ const image = opts.agentImage ?? DEFAULT_AGENT_IMAGE;
81
+ return `docker run -d --restart=no --name kici-agent --network host --env-file ${AGENT_ENV_FILE} ${image}`;
82
+ }
83
+ /**
84
+ * Build the cloud-config `user_data` string. In the token form the agent token
85
+ * appears only in the reserved env-file write entry (`0600`, root-only); the
86
+ * claim-code form keeps the token off the provisioning channel entirely.
87
+ */
88
+ function buildAgentCloudInit(creds, options) {
89
+ validateAgentEnv(options.agentEnv);
90
+ assertNoReservedWrite(options.writeFiles, "writeFiles");
91
+ const cap = Math.max(1, Math.floor(options.maxLifetimeMinutes));
92
+ const base = options.baseCloudConfig ? parseBase(options.baseCloudConfig) : {};
93
+ assertNoReservedWrite(base.write_files, "baseCloudConfig");
94
+ const baseWriteFiles = base.write_files ?? [];
95
+ const baseRuncmd = base.runcmd ?? [];
96
+ const basePackages = base.packages ?? [];
97
+ const envFile = {
98
+ path: AGENT_ENV_FILE,
99
+ permissions: "0600",
100
+ owner: "root:root",
101
+ content: renderEnvFileContent(creds, options.agentEnv)
102
+ };
103
+ const pkgs = dedupe([...basePackages, ...options.packages ?? []]);
104
+ const model = {
105
+ ...base,
106
+ write_files: [
107
+ ...baseWriteFiles,
108
+ envFile,
109
+ ...options.writeFiles ?? []
110
+ ],
111
+ runcmd: [
112
+ ...baseRuncmd,
113
+ ...options.runcmdBefore ?? [],
114
+ `systemd-run --on-active=${cap}m --timer-property=AccuracySec=1s /sbin/poweroff`,
115
+ renderStartCommand(options),
116
+ ...options.runcmdAfter ?? []
117
+ ]
118
+ };
119
+ if (pkgs.length > 0) model.packages = pkgs;
120
+ else delete model.packages;
121
+ const cloudConfig = `#cloud-config\n${stringify(model, { lineWidth: 0 })}`;
122
+ if (options.userDataEncoding === "base64") return Buffer.from(cloudConfig, "utf8").toString("base64");
123
+ return cloudConfig;
124
+ }
125
+ //#endregion
126
+ export { buildAgentCloudInit };
127
+
128
+ //# sourceMappingURL=agent-cloud-init.js.map
@@ -9,6 +9,7 @@
9
9
  * 2. Register the handler in the orchestrator's AgentApiRegistry
10
10
  */
11
11
  import { type OidcTokenResult } from '@kici-dev/engine/protocol/messages/oidc-token-relay';
12
+ import type { GitApi } from './git-types.js';
12
13
  import type { HostInventoryEntry, InventorySelector } from '@kici-dev/engine';
13
14
  export type { OidcTokenResult };
14
15
  export type { HostInventoryEntry, InventorySelector };
@@ -136,6 +137,27 @@ export interface BootstrapApi {
136
137
  restaged: boolean;
137
138
  }>;
138
139
  }
140
+ /** Credentials a provisioning workflow uses to boot a scaler-provisioned agent. */
141
+ export interface ClaimedAgentCredentials {
142
+ /** Single-use ephemeral agent token the provisioned instance registers with. */
143
+ agentToken: string;
144
+ /** Agent id the instance must register with (chosen by the scaler). */
145
+ agentId: string;
146
+ /** Orchestrator WS URL the instance connects back to. */
147
+ orchestratorUrl: string;
148
+ /** Labels the token authorizes. */
149
+ labels: string[];
150
+ }
151
+ export interface ScalerApi {
152
+ /**
153
+ * Exchange a single-use claim code — delivered on a `kici.scaler.scale-up`
154
+ * event to a provisioning workflow — for freshly minted ephemeral agent
155
+ * credentials. Boot a cloud instance whose agent registers with the returned
156
+ * `agentId` and `agentToken`, and the pending bound job runs on it. The token
157
+ * is minted lazily on this call and never appears in the persisted event log.
158
+ */
159
+ claimAgentCredentials(claimCode: string): Promise<ClaimedAgentCredentials>;
160
+ }
139
161
  export interface KiciApi {
140
162
  /** Query orchestrator infrastructure (scalers, agents). */
141
163
  infrastructure: InfrastructureApi;
@@ -143,10 +165,14 @@ export interface KiciApi {
143
165
  inventory: InventoryApi;
144
166
  /** Request short-lived OIDC ID tokens for the current job (build provenance). */
145
167
  oidc: OidcApi;
168
+ /** Forge-typed git credentials for the current job. */
169
+ git: GitApi;
146
170
  /** Host-lifecycle operations on the agent's own host (e.g. reboot). */
147
171
  host: HostApi;
148
172
  /** Fresh-box bootstrap bring-up (init-runner over SSH, pre-boot unlock). */
149
173
  bootstrap: BootstrapApi;
174
+ /** Event-scaler provisioning: claim ephemeral agent credentials. */
175
+ scaler: ScalerApi;
150
176
  }
151
177
  /**
152
178
  * Low-level transport function used to implement KiciApi.
package/dist/api-types.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
2
  import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
3
+ import { GIT_CREDENTIAL_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/git-credential-relay";
3
4
  //#region src/api-types.ts
4
5
  /**
5
6
  * Typed KiCI API available to workflows via ctx.kici.
@@ -37,6 +38,23 @@ function buildKiciApi(transport, jobCtx) {
37
38
  audience: opts.audience
38
39
  });
39
40
  } },
41
+ git: { github: { getToken: (opts) => {
42
+ if (!jobCtx) return Promise.reject(/* @__PURE__ */ new Error("ctx.kici.git.github.getToken() is only available inside a running job step"));
43
+ if (opts.repositories.length === 0) return Promise.reject(/* @__PURE__ */ new Error("git.github.getToken requires at least one repository"));
44
+ return transport(GIT_CREDENTIAL_REQUEST_METHOD, {
45
+ jobId: jobCtx.jobId,
46
+ repositories: opts.repositories,
47
+ permissions: opts.permissions,
48
+ ...opts.credential ? { credential: opts.credential } : {}
49
+ }).then((raw) => {
50
+ const r = raw;
51
+ return {
52
+ token: r.secret,
53
+ expiresAt: r.expiresAt,
54
+ granted: r.grant
55
+ };
56
+ });
57
+ } } },
40
58
  host: { requestReboot: (opts) => transport("host.requestReboot", { ...opts?.deadlineMs !== void 0 ? { deadlineMs: opts.deadlineMs } : {} }) },
41
59
  bootstrap: {
42
60
  ensureInitRunner: (targetAgentId) => transport("kici.ensureInitRunner", { targetAgentId }),
@@ -46,7 +64,8 @@ function buildKiciApi(transport, jobCtx) {
46
64
  }),
47
65
  agentVersionStatus: (targetAgentId) => transport("kici.agentVersionStatus", { targetAgentId }),
48
66
  restageAgent: (targetAgentId) => transport("kici.restageAgent", { targetAgentId })
49
- }
67
+ },
68
+ scaler: { claimAgentCredentials: (claimCode) => transport("scaler.claim-credentials", { claimCode }) }
50
69
  };
51
70
  }
52
71
  //#endregion
package/dist/context.d.ts CHANGED
@@ -189,6 +189,27 @@ export interface StepContext<TInputs = Record<string, unknown>> {
189
189
  * Defaults to false for backward compatibility.
190
190
  */
191
191
  isTestRun: boolean;
192
+ /**
193
+ * The job's own checked-out repository.
194
+ *
195
+ * Present for every job that checks out (unlike `sourceRepo` / `workflowRepo`,
196
+ * which exist only for a global workflow). `withWrite` opens a write window
197
+ * for THIS repository, bounded by the repository and by the callback's
198
+ * duration — not by the step: steps running concurrently in the same job can
199
+ * push to the same repository while it is open, but cannot reach a different
200
+ * one. It throws before any git runs when the forge will not grant what was
201
+ * requested.
202
+ */
203
+ repo?: {
204
+ identifier: string;
205
+ path: string;
206
+ ref?: string;
207
+ sha?: string;
208
+ withWrite(opts: {
209
+ permissions?: Record<string, string>;
210
+ credential?: string;
211
+ }, fn: () => Promise<void>): Promise<void>;
212
+ };
192
213
  /**
193
214
  * Workflow repo metadata -- only set for global workflows.
194
215
  * The registering repo where the workflow code is defined.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=filter.test-d.d.ts.map
@@ -0,0 +1,30 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ import { workflow } from "./workflow.js";
3
+ import { job } from "./job.js";
4
+ import { step } from "./step.js";
5
+ import { describe, expectTypeOf, it } from "vitest";
6
+ //#region src/filter.test-d.ts
7
+ const noop = job("noop", {
8
+ runsOn: "linux",
9
+ steps: [step("x", { run: async () => {} })]
10
+ });
11
+ describe("workflow filter — type level", () => {
12
+ it("rejects a non-function filter", () => {
13
+ workflow("org-ci", {
14
+ jobs: [noop],
15
+ filter: "yes"
16
+ });
17
+ });
18
+ it("accepts a function filter", () => {
19
+ const filter = ({ sourceRepo }) => sourceRepo.identifier !== "a/b";
20
+ const wf = workflow("org-ci", {
21
+ jobs: [noop],
22
+ filter
23
+ });
24
+ expectTypeOf(wf.filter).toEqualTypeOf();
25
+ });
26
+ });
27
+ //#endregion
28
+ export {};
29
+
30
+ //# sourceMappingURL=filter.test-d.js.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Forge-typed git surface for workflow authors.
3
+ *
4
+ * The forge is a TYPE PARAMETER and nothing more. It is erased at runtime:
5
+ * `kici.git.clone<'github'>()` against a Forgejo source compiles cleanly, the
6
+ * requested permissions are ignored, and the result reports
7
+ * `granted: { scoped: false }`. It buys autocomplete and a compile error on a
8
+ * wrong-shaped permission object; it buys no safety. The runtime `granted`
9
+ * value is the only source of truth about what a credential can do.
10
+ */
11
+ /** Forges that can back a git credential. Mirrors the engine's `ForgeName`. */
12
+ export type ForgeName = 'github' | 'gitlab' | 'bitbucket' | 'generic';
13
+ /**
14
+ * GitHub repository permissions.
15
+ *
16
+ * DELIBERATE CARVE-OUT from the enums-over-hardcoded-strings rule: the known
17
+ * keys below are typed so editors autocomplete them, but an index signature
18
+ * lets an unknown key through. GitHub adds permissions over time, and a closed
19
+ * enum would mean a permission shipped yesterday needs an SDK release — and the
20
+ * SDK is compat-protected and rides the single-version release train. Unknown
21
+ * keys are passed to GitHub verbatim for it to accept or reject.
22
+ */
23
+ export interface GitHubPermissions {
24
+ contents?: 'read' | 'write';
25
+ metadata?: 'read';
26
+ workflows?: 'write';
27
+ actions?: 'read' | 'write';
28
+ pull_requests?: 'read' | 'write';
29
+ issues?: 'read' | 'write';
30
+ checks?: 'read' | 'write';
31
+ statuses?: 'read' | 'write';
32
+ deployments?: 'read' | 'write';
33
+ packages?: 'read' | 'write';
34
+ [permission: string]: string | undefined;
35
+ }
36
+ /**
37
+ * Options for a write elevation, shaped by the forge.
38
+ *
39
+ * `WriteOptions<'generic'>` carries no permission fields at all: a static
40
+ * credential has nothing to request, because an SSH key or PAT is read-write or
41
+ * it is not.
42
+ */
43
+ export type WriteOptions<F extends ForgeName = 'github'> = F extends 'github' ? {
44
+ permissions: GitHubPermissions;
45
+ } : Record<never, never>;
46
+ /** What a credential turned out to be able to do. Never an echo of the request. */
47
+ export type GitGrant = {
48
+ scoped: false;
49
+ } | {
50
+ scoped: true;
51
+ permissions: Record<string, string>;
52
+ };
53
+ /**
54
+ * One half of a credential field pair. Exactly one form is set: a qualified
55
+ * `<context>:<secret-name>` reference resolved from the secrets backend, or
56
+ * material supplied at runtime.
57
+ *
58
+ * The field NAME is the discriminator, following the convention `workflow.ts`
59
+ * already sets with `registries[].tokenSecret` and `isQualifiedSecretRef`.
60
+ */
61
+ export type Sourced<Name extends string> = {
62
+ [K in `${Name}Secret`]: string;
63
+ } | {
64
+ [K in `${Name}Value`]: string;
65
+ };
66
+ /** Where credential material comes from, for each supported credential shape. */
67
+ export type GitCredentialRef = ({
68
+ kind: 'app';
69
+ } & Sourced<'appId'> & Sourced<'installationId'> & Sourced<'privateKey'>) | ({
70
+ kind: 'token';
71
+ user?: string;
72
+ } & Sourced<'token'>) | ({
73
+ kind: 'ssh';
74
+ } & Sourced<'privateKey'>);
75
+ /** Named credentials for a job. `default` is used when a call names none. */
76
+ export type GitCredentialMap = Record<string, GitCredentialRef>;
77
+ /**
78
+ * Resolve a call site's credential.
79
+ *
80
+ * Order: an explicit per-call value wins; otherwise `default` from the job map;
81
+ * otherwise undefined, which means the source credential — all a read needs.
82
+ *
83
+ * An unknown name THROWS rather than falling back to `default`: silently using
84
+ * a different credential than the author named is precisely the confusion this
85
+ * map exists to remove.
86
+ */
87
+ export declare function resolveCredential(perCall: string | GitCredentialRef | undefined, map: GitCredentialMap | undefined): GitCredentialRef | undefined;
88
+ /**
89
+ * Every credential field names a secret. Catch the easy, silent mistake of
90
+ * pasting the credential itself — which would commit it to a git repository.
91
+ */
92
+ export declare function assertSecretName(value: string, field: string, subject?: string): void;
93
+ /**
94
+ * Private-registry credentials for pulling a job's container image.
95
+ *
96
+ * Built from `Sourced<Name>` so "this field names a secret" has ONE spelling
97
+ * across the SDK — the same one `gitCredentials` uses. `username` may be a
98
+ * plain string (a registry username is not a secret); the token may not be.
99
+ */
100
+ export type ContainerRegistryAuth = Sourced<'token'> & {
101
+ /** Plain registry username. Mutually exclusive with `usernameSecret`. */
102
+ username?: string;
103
+ /**
104
+ * Registry host these credentials belong to (e.g. `reg.internal:5000`).
105
+ *
106
+ * Optional with `container.image`, where it is derived from the image
107
+ * reference. REQUIRED with `container.dockerfile`: the base image is named
108
+ * inside the Dockerfile, so there is nothing to derive it from.
109
+ */
110
+ registry?: string;
111
+ } & Partial<Sourced<'username'>>;
112
+ /** A checked-out repository. The forge travels with the handle. */
113
+ export interface RepoHandle<F extends ForgeName = 'github'> {
114
+ /** `owner/repo`. */
115
+ identifier: string;
116
+ /** Absolute path to the working tree. */
117
+ path: string;
118
+ ref?: string;
119
+ sha?: string;
120
+ /**
121
+ * Run `fn` with write credentials for THIS repository.
122
+ *
123
+ * The grant is scoped to this repository and to the duration of `fn` — NOT to
124
+ * the calling step. The agent runs one process per job and `parallel()` runs
125
+ * its children inside it, so a concurrent sibling step can push to the same
126
+ * repository while the grant is live. It cannot reach a different repository.
127
+ *
128
+ * Throws at entry — before any git runs — when the forge grants less than was
129
+ * requested, naming the missing permission.
130
+ */
131
+ withWrite(opts: WriteOptions<F>, fn: () => Promise<void>): Promise<void>;
132
+ }
133
+ /** Result of an explicit token request. */
134
+ export interface GitTokenResult {
135
+ token: string;
136
+ expiresAt: string | null;
137
+ granted: GitGrant;
138
+ }
139
+ /** Forge-specific minting. Only minted shapes appear here; static ones have nothing to mint. */
140
+ export interface GitHubApi {
141
+ /**
142
+ * Mint a token as a VALUE, for the forge API, the `gh` CLI, or a third-party
143
+ * tool. Auto-masked in step logs; job-bound; throws outside a running step.
144
+ *
145
+ * For git operations prefer `handle.withWrite()`, which never puts a token in
146
+ * the step environment. `gh` does not read git credential helpers, which is
147
+ * why this exists.
148
+ */
149
+ getToken(opts: {
150
+ repositories: string[];
151
+ permissions: GitHubPermissions;
152
+ /**
153
+ * Name an entry in the job's `gitCredentials` map. Omit to use `default`;
154
+ * omit both and the source credential applies.
155
+ */
156
+ credential?: string;
157
+ }): Promise<GitTokenResult>;
158
+ }
159
+ export interface GitApi {
160
+ github: GitHubApi;
161
+ }
162
+ //# sourceMappingURL=git-types.d.ts.map
@@ -0,0 +1,32 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ //#region src/git-types.ts
3
+ /**
4
+ * Resolve a call site's credential.
5
+ *
6
+ * Order: an explicit per-call value wins; otherwise `default` from the job map;
7
+ * otherwise undefined, which means the source credential — all a read needs.
8
+ *
9
+ * An unknown name THROWS rather than falling back to `default`: silently using
10
+ * a different credential than the author named is precisely the confusion this
11
+ * map exists to remove.
12
+ */
13
+ function resolveCredential(perCall, map) {
14
+ if (perCall && typeof perCall === "object") return perCall;
15
+ if (typeof perCall === "string") {
16
+ const found = map?.[perCall];
17
+ if (!found) throw new Error(`Unknown git credential '${perCall}'. Declare it in the job's gitCredentials map. Known: ${Object.keys(map ?? {}).join(", ") || "(none)"}`);
18
+ return found;
19
+ }
20
+ return map?.default;
21
+ }
22
+ /**
23
+ * Every credential field names a secret. Catch the easy, silent mistake of
24
+ * pasting the credential itself — which would commit it to a git repository.
25
+ */
26
+ function assertSecretName(value, field, subject = "git credential") {
27
+ if (value.startsWith("-----BEGIN") || /^gh[pousr]_/.test(value) || value.startsWith("github_pat_")) throw new Error(`${subject} '${field}' looks like the credential itself, not the name of a secret holding it. Store it with \`kici-admin secret set\` and name it here.`);
28
+ }
29
+ //#endregion
30
+ export { assertSecretName, resolveCredential };
31
+
32
+ //# sourceMappingURL=git-types.js.map
@@ -83,7 +83,7 @@ function idempotentStep(name, opts) {
83
83
  * lockfile change.
84
84
  */
85
85
  function checkStep(name, options) {
86
- return step(name, {
86
+ const checkOptions = {
87
87
  check: options.check,
88
88
  summarize: options.summarize,
89
89
  run: (ctx, drift) => options.apply(ctx, drift),
@@ -94,7 +94,8 @@ function checkStep(name, options) {
94
94
  ...options.retry !== void 0 && { retry: options.retry },
95
95
  ...options.cache !== void 0 && { cache: options.cache },
96
96
  ...options.rules !== void 0 && { rules: options.rules }
97
- });
97
+ };
98
+ return step(name, checkOptions);
98
99
  }
99
100
  //#endregion
100
101
  export { checkStep, idempotent, idempotentStep };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { job } from './job.js';
3
3
  export { workflow } from './workflow.js';
4
4
  export { parallel, isParallelGroup, flattenStepInputs } from './parallel.js';
5
5
  export type { ParallelGroup, ParallelOptions } from './parallel.js';
6
+ export { invokeSource } from './invoke.js';
7
+ export type { InvokeConfig } from './invoke.js';
6
8
  export { normalizeApproval } from './approval.js';
7
9
  export type { ApprovalConfig, ApprovalWhen, ApproverClause, NormalizedApproval, } from './approval.js';
8
10
  export { pr, push, tag, comment, review, reviewComment, release, dispatch, create, delete as delete, status, workflowRun, fork, star, watch, webhook, kiciEvent, workflowComplete, workflowsFailedBatch, jobComplete, genericWebhook, schedule, lifecycle, defineDispatchInputs, } from './triggers/index.js';
@@ -21,7 +23,7 @@ export type { SourceLocation, OutputProxy, Step, StepOptions, StepOptionsBase, S
21
23
  export { isDynamicJobFn, dynamicJob, getDynamicJobGroup, getDynamicJobNeeds, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, } from './types.js';
22
24
  export type { TaggedDynamicJobFn, ResultAwareDynamicJobConfig, ResultAwareDynamicJobFn, DynamicJobNeed, NeedsWhen, NeedsWhenInput, } from './types.js';
23
25
  export { buildNeedsContext } from './needs-context.js';
24
- export type { UpstreamSnapshot, NeedsContext, NeedEntry, GroupNeedEntry } from './needs-context.js';
26
+ export type { UpstreamSnapshot, NeedsContext, NeedEntry, GroupNeedEntry, InvokeNeedEntry, InvokeResult, } from './needs-context.js';
25
27
  export { CacheSpecSchema, normalizeCacheSpecs } from './cache-types.js';
26
28
  export type { CacheSpec, CacheInput } from './cache-types.js';
27
29
  export type { CacheRestoreResult, CacheApi } from './cache-types.js';
@@ -61,4 +63,9 @@ export type { WaitForHostAliveOptions, RestartHostOptions } from './host-restart
61
63
  export { agentVersionConverge } from './fleet/agent-version-converge.js';
62
64
  export type { AgentVersionConvergeOptions, AgentVersionDrift, } from './fleet/agent-version-converge.js';
63
65
  export { z } from 'zod';
66
+ export { SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleUpPayload, ScalerScaleDownPayload, } from '@kici-dev/engine';
67
+ export { buildAgentCloudInit } from './agent-cloud-init.js';
68
+ export type { CloudInitCredentials, ClaimCodeCredentials, AgentCloudInitCredentials, AgentDeliveryMode, UserDataEncoding, CloudInitWriteFile, AgentCloudInitOptions, } from './agent-cloud-init.js';
69
+ export { assertSecretName } from './git-types.js';
70
+ export type { ForgeName, GitHubPermissions, WriteOptions, GitGrant, Sourced, ContainerRegistryAuth, GitCredentialRef, GitCredentialMap, RepoHandle, GitTokenResult, GitHubApi, GitApi, } from './git-types.js';
64
71
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
+ import { buildAgentCloudInit } from "./agent-cloud-init.js";
2
3
  import { buildKiciApi } from "./api-types.js";
3
4
  import { normalizeApproval } from "./approval.js";
4
5
  import { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema } from "./artifacts-types.js";
@@ -8,15 +9,17 @@ import { DYNAMIC_GROUP_TAG, dynamicGroup, isDynamicGroupRef } from "./dynamic-gr
8
9
  import { SecretNotFoundError } from "./errors.js";
9
10
  import { ChangedFilesUnavailableError } from "./rules/changed-files.js";
10
11
  import { createFilterContext } from "./filter-context.js";
11
- import { fixture } from "./fixture.js";
12
+ import { workflow } from "./workflow.js";
12
13
  import { createJobOutputProxy, createSnapshotOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "./outputs.js";
14
+ import { assertSecretName } from "./git-types.js";
15
+ import { job } from "./job.js";
13
16
  import { step } from "./step.js";
17
+ import { fixture } from "./fixture.js";
14
18
  import { WaitForTimeoutError, waitFor, waitForStep } from "./wait-for.js";
15
19
  import { restartHost, waitForHostAlive } from "./host-restart.js";
16
20
  import { checkStep, idempotent, idempotentStep } from "./idempotent.js";
17
- import { job } from "./job.js";
18
- import { workflow } from "./workflow.js";
19
21
  import { flattenStepInputs, isParallelGroup, parallel } from "./parallel.js";
22
+ import { invokeSource } from "./invoke.js";
20
23
  import { pr } from "./triggers/pr.js";
21
24
  import { push } from "./triggers/push.js";
22
25
  import { tag } from "./triggers/tag.js";
@@ -60,5 +63,6 @@ import "./matrix/index.js";
60
63
  import { defineEvent, isEventDefinition } from "./events/define-event.js";
61
64
  import "./events/index.js";
62
65
  import { agentVersionConverge } from "./fleet/agent-version-converge.js";
66
+ import { SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleDownPayload, ScalerScaleUpPayload } from "@kici-dev/engine";
63
67
  import { z } from "zod";
64
- export { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema, CacheSpecSchema, ChangedFilesUnavailableError, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, SecretNotFoundError, WaitForTimeoutError, afterStep, agentVersionConverge, applyIncludeExclude, beforeStep, buildKiciApi, buildNeedsContext, checkStep, cleanup, comment, create, createFilterContext, createJobOutputProxy, createRuleContext, createSnapshotOutputProxy, createStepOutputProxy, createStepSecrets, defineDispatchInputs, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, flattenStepInputs, fork, genericWebhook, getDynamicJobGroup, getDynamicJobNeeds, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventDefinition, isEventType, isHostJobOutputs, isMatrixJobOutputs, isParallelGroup, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeApproval, normalizeCacheSpecs, onCancel, onFailure, onSuccess, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, parallel, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, restartHost, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForHostAlive, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, workflowsFailedBatch, z };
68
+ export { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema, CacheSpecSchema, ChangedFilesUnavailableError, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleDownPayload, ScalerScaleUpPayload, SecretNotFoundError, WaitForTimeoutError, afterStep, agentVersionConverge, applyIncludeExclude, assertSecretName, beforeStep, buildAgentCloudInit, buildKiciApi, buildNeedsContext, checkStep, cleanup, comment, create, createFilterContext, createJobOutputProxy, createRuleContext, createSnapshotOutputProxy, createStepOutputProxy, createStepSecrets, defineDispatchInputs, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, flattenStepInputs, fork, genericWebhook, getDynamicJobGroup, getDynamicJobNeeds, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, invokeSource, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventDefinition, isEventType, isHostJobOutputs, isMatrixJobOutputs, isParallelGroup, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeApproval, normalizeCacheSpecs, onCancel, onFailure, onSuccess, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, parallel, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, restartHost, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForHostAlive, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, workflowsFailedBatch, z };
@@ -0,0 +1,29 @@
1
+ /** Config produced by {@link invokeSource}: emit a kici event at the source repo and gate on the runs it triggers. */
2
+ export interface InvokeConfig {
3
+ readonly _tag: 'InvokeSource';
4
+ /** The kici event name to emit. Subscribers opt in with `kiciEvent({ name })`. */
5
+ readonly event: string;
6
+ /** Target scope. `'source'` targets exactly `ctx.sourceRepo` (the only v1 scope). */
7
+ readonly scope: 'source';
8
+ /** Optional event payload delivered to subscribers. */
9
+ readonly payload?: Readonly<Record<string, unknown>>;
10
+ /**
11
+ * When true, a zero-subscriber emit succeeds immediately (the repo may opt out).
12
+ * When false/unset (the default), a zero-subscriber emit FAILS the gate — a repo
13
+ * that never wired up its tests must not silently pass the org gate.
14
+ */
15
+ readonly optional?: boolean;
16
+ }
17
+ /**
18
+ * Invoke the source repo's opt-in workflows and gate on them.
19
+ *
20
+ * Targets exactly `ctx.sourceRepo`, so a global workflow can hand control back to
21
+ * the repo whose event triggered it without any org-wide fan-out. Subscribers opt
22
+ * in with `kiciEvent({ name })`. Required by default: if no workflow subscribes,
23
+ * the gate fails unless `optional: true` is set.
24
+ */
25
+ export declare function invokeSource(event: string, opts?: {
26
+ payload?: Record<string, unknown>;
27
+ optional?: boolean;
28
+ }): InvokeConfig;
29
+ //# sourceMappingURL=invoke.d.ts.map