@kici-dev/engine 0.4.0 → 0.6.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.
Files changed (81) hide show
  1. package/dist/audit/access-log-policy.js +1 -0
  2. package/dist/audit/retention-policy.js +2 -0
  3. package/dist/context/held-run-job-id.d.ts +26 -10
  4. package/dist/context/held-run-job-id.js +30 -11
  5. package/dist/context/host-match.js +2 -1
  6. package/dist/context/index.d.ts +1 -1
  7. package/dist/context/index.js +3 -3
  8. package/dist/context/types.d.ts +10 -1
  9. package/dist/context/types.js +10 -1
  10. package/dist/index.d.ts +6 -2
  11. package/dist/index.js +17 -12
  12. package/dist/labels/compile.d.ts +2 -7
  13. package/dist/labels/compile.js +1 -10
  14. package/dist/labels.d.ts +74 -12
  15. package/dist/labels.js +82 -17
  16. package/dist/mcp/held-run-resolve.d.ts +40 -0
  17. package/dist/mcp/held-run-resolve.js +68 -15
  18. package/dist/mcp/tool-schemas.d.ts +4 -0
  19. package/dist/mcp/tool-schemas.js +13 -1
  20. package/dist/metrics/catalog-policy.d.ts +17 -3
  21. package/dist/metrics/catalog-policy.js +44 -13
  22. package/dist/metrics/metric-catalog.generated.d.ts +140 -0
  23. package/dist/metrics/metric-catalog.generated.js +168 -0
  24. package/dist/protocol/dashboard-global-workflows.js +2 -2
  25. package/dist/protocol/event-log-payload.js +1 -1
  26. package/dist/protocol/messages/access-log.d.ts +5 -0
  27. package/dist/protocol/messages/access-log.js +1 -0
  28. package/dist/protocol/messages/actor.d.ts +13 -2
  29. package/dist/protocol/messages/actor.js +16 -5
  30. package/dist/protocol/messages/common.js +1 -1
  31. package/dist/protocol/messages/dashboard-global-workflows.d.ts +29 -3
  32. package/dist/protocol/messages/dashboard-global-workflows.js +48 -4
  33. package/dist/protocol/messages/dashboard.d.ts +48 -7
  34. package/dist/protocol/messages/dashboard.js +62 -6
  35. package/dist/protocol/messages/execution-status.d.ts +44 -0
  36. package/dist/protocol/messages/execution-status.js +69 -2
  37. package/dist/protocol/messages/git-credential-relay.d.ts +78 -0
  38. package/dist/protocol/messages/git-credential-relay.js +86 -0
  39. package/dist/protocol/messages/orchestrator-agent.d.ts +138 -0
  40. package/dist/protocol/messages/orchestrator-agent.js +131 -3
  41. package/dist/protocol/messages/peer.d.ts +7 -0
  42. package/dist/protocol/messages/peer.js +18 -1
  43. package/dist/protocol/messages/platform-orchestrator.d.ts +155 -2
  44. package/dist/protocol/messages/platform-orchestrator.js +178 -19
  45. package/dist/protocol/version.d.ts +19 -2
  46. package/dist/protocol/version.js +20 -3
  47. package/dist/provenance/verify.js +11 -10
  48. package/dist/provider/check-status-poster.d.ts +37 -2
  49. package/dist/provider/contributor-resolver.d.ts +11 -3
  50. package/dist/provider/file-contents-fetcher.d.ts +39 -0
  51. package/dist/provider/file-contents-fetcher.js +2 -0
  52. package/dist/provider/git-credential.d.ts +77 -0
  53. package/dist/provider/git-credential.js +10 -0
  54. package/dist/provider/index.d.ts +4 -0
  55. package/dist/provider/index.js +2 -1
  56. package/dist/provider/webhook-normalizer.d.ts +12 -12
  57. package/dist/repo/pattern-negation.d.ts +73 -0
  58. package/dist/repo/pattern-negation.js +86 -0
  59. package/dist/safe-regex.d.ts +16 -0
  60. package/dist/safe-regex.js +24 -0
  61. package/dist/scaler/registry-auth.d.ts +18 -0
  62. package/dist/scaler/registry-auth.js +28 -0
  63. package/dist/scaler/scaler-backend-type.d.ts +35 -0
  64. package/dist/scaler/scaler-backend-type.js +39 -2
  65. package/dist/scaler/scaler-events.d.ts +79 -0
  66. package/dist/scaler/scaler-events.js +87 -0
  67. package/dist/trigger/compiled-matchers.d.ts +21 -1
  68. package/dist/trigger/compiled-matchers.js +30 -3
  69. package/dist/trigger/content-requirements.d.ts +31 -0
  70. package/dist/trigger/content-requirements.js +125 -0
  71. package/dist/trigger/decision-trace.d.ts +168 -0
  72. package/dist/trigger/decision-trace.js +207 -4
  73. package/dist/trigger/jsonpath-matcher.js +5 -1
  74. package/dist/trigger/matcher.js +61 -10
  75. package/dist/trigger/text-match.d.ts +27 -0
  76. package/dist/trigger/text-match.js +86 -0
  77. package/dist/trigger/types.d.ts +265 -23
  78. package/dist/trigger/types.js +37 -5
  79. package/dist/ws/rate-limiter.js +3 -3
  80. package/package.json +20 -3
  81. package/sbom.spdx.json +40 -15
package/dist/labels.d.ts CHANGED
@@ -175,6 +175,38 @@ export declare function capabilityLabel(name: string): string;
175
175
  * the bootstrap SSH key.
176
176
  */
177
177
  export declare const SSH_TRANSPORT_CAPABILITY = "kici:capability:ssh-transport";
178
+ /**
179
+ * Runtime-fact label prefix — `kici:runtime:<name>` states something the AGENT
180
+ * discovered about its own host.
181
+ *
182
+ * Deliberately NOT `kici:capability:`. That prefix grants a privilege
183
+ * (`ssh-transport` custodies the bootstrap SSH key), so it must stay
184
+ * token-bound and an agent must never be able to self-assert it. A runtime fact
185
+ * grants nothing: it reports what the host can do, the way `kici:os:` reports
186
+ * what the host is, and it is self-reported for the same reason — the scaler
187
+ * cannot predict it at token-mint time.
188
+ */
189
+ export declare const RUNTIME_LABEL_PREFIX = "kici:runtime:";
190
+ /** Build a `kici:runtime:<name>` label. */
191
+ export declare function runtimeLabel(name: string): string;
192
+ /**
193
+ * Runtime facts an agent reports about its own host at registration.
194
+ *
195
+ * - `docker` / `podman` — that container runtime's socket answered, so the
196
+ * agent can nest a job container.
197
+ * - `container-build` — a `docker` / `podman` CLI is on PATH, so the agent can
198
+ * build a job's image from a Dockerfile. Distinct from the two above: a host
199
+ * reachable only through a mounted socket can RUN containers but not BUILD
200
+ * one, because the build shells out to the CLI.
201
+ */
202
+ export declare const RuntimeFact: z.ZodEnum<{
203
+ "container-build": "container-build";
204
+ docker: "docker";
205
+ podman: "podman";
206
+ }>;
207
+ export type RuntimeFact = z.infer<typeof RuntimeFact>;
208
+ /** The label a job's `runsOn` names to require a host that can build an image. */
209
+ export declare const CONTAINER_BUILD_RUNTIME_LABEL: string;
178
210
  /**
179
211
  * The `kici:init` lifecycle label carried by a temporary init-runner agent
180
212
  * brought up on a fresh box for bootstrap. Marks it as ephemeral + privileged;
@@ -194,6 +226,14 @@ export declare const ROLE_LABEL_PREFIX = "kici:role:";
194
226
  * Convert a role name to its corresponding label.
195
227
  */
196
228
  export declare function roleToLabel(role: AgentRole): string;
229
+ /**
230
+ * The label every pre-run evaluation job routes to.
231
+ *
232
+ * Exported because three dispatch sites and the global eval round all target
233
+ * it, and each had spelled the string out. Derived from {@link roleToLabel} so
234
+ * it cannot drift from the prefix or from `KNOWN_ROLES`.
235
+ */
236
+ export declare const INIT_RUNNER_ROLE_LABEL: string;
197
237
  /**
198
238
  * Resolve a roles configuration into role labels.
199
239
  *
@@ -205,22 +245,44 @@ export declare function roleToLabel(role: AgentRole): string;
205
245
  export declare function resolveRoleLabels(roles: readonly string[] | undefined): string[];
206
246
  /**
207
247
  * The full label set a scaler injects into an agent it spawns: the base label
208
- * set plus the scaler-assigned `kici:agent:`, `kici:scaler:`, and `kici:role:`
209
- * labels. This is exactly what the scaler writes into `KICI_LABELS`, and it is
210
- * the set an ephemeral agent token must be bound to — the agent then adds only
211
- * the self-reported platform facts (`kici:os:`, `kici:arch:`, `kici:host:`) at
212
- * registration, which the orchestrator's register-time scope gate exempts (see
213
- * `isSelfReportedLabel`). Keep this aligned with the agent's wire-label
214
- * construction in `orchestrator-client.ts#sendAgentRegister`.
248
+ * set plus the scaler-assigned `kici:agent:`, `kici:scaler:`, `kici:role:`, and
249
+ * platform-taint labels. This is exactly what the scaler writes into
250
+ * `KICI_LABELS`, and it is the set an ephemeral agent token must be bound to —
251
+ * the agent then adds only the self-reported platform facts (`kici:os:`,
252
+ * `kici:arch:`, `kici:host:`) at registration, which the orchestrator's
253
+ * register-time scope gate exempts (see `isSelfReportedLabel`). Keep this
254
+ * aligned with the agent's wire-label construction in
255
+ * `orchestrator-client.ts#sendAgentRegister`.
256
+ *
257
+ * `platformTaints` are the plain tokens (`windows`, `macos`, `arm64`) derived
258
+ * from the pool's resolved platform. A taint is a routing GRANT, not a host
259
+ * fact: it is what a job's `runsOn` names to reach this pool, and the matcher
260
+ * gates on it. So the orchestrator asserts it here — bound into the agent's
261
+ * ephemeral token — rather than letting the agent self-report it. Adding these
262
+ * prefixes to `SELF_REPORTED_LABEL_PREFIXES` instead would let any agent claim
263
+ * any platform unchallenged, which is the opposite of a gate. The caller must
264
+ * derive them from the same source the gate uses
265
+ * (`ScalerManager.resolveScalerPlatform`) — deriving them twice is exactly the
266
+ * defect this parameter closes.
267
+ *
268
+ * The result is de-duplicated in first-seen order: a warm pool measures its
269
+ * readiness with a label set that already carries the taints, so it passes them
270
+ * as `labelSet` AND as `platformTaints`, and a label repeated in the set an
271
+ * ephemeral token is bound to is never meaningful.
215
272
  */
216
- export declare function scalerAgentLabels(labelSet: string[], backendType: string, backendName: string, roles: readonly string[] | undefined): string[];
273
+ export declare function scalerAgentLabels(labelSet: string[], backendType: string, backendName: string, roles: readonly string[] | undefined, platformTaints?: readonly string[]): string[];
217
274
  /**
218
275
  * Label-category prefixes the agent self-reports at registration from its own
219
- * host (operating system, CPU architecture, hostname). They are immutable
220
- * platform facts, not authorization grants, so the orchestrator's register-time
221
- * label-scope gate does not require an ephemeral token to be bound to them.
276
+ * host: operating system, CPU architecture, hostname, and runtime facts. They
277
+ * are platform facts, not authorization grants, so the orchestrator's
278
+ * register-time label-scope gate does not require an ephemeral token to be
279
+ * bound to them.
280
+ *
281
+ * Adding a prefix here lets any agent assert it unchallenged. That is correct
282
+ * for a fact about the host and WRONG for anything that grants a privilege —
283
+ * `kici:capability:` is absent from this list for exactly that reason.
222
284
  */
223
- export declare const SELF_REPORTED_LABEL_PREFIXES: readonly ['kici:os:', 'kici:arch:', 'kici:host:'];
285
+ export declare const SELF_REPORTED_LABEL_PREFIXES: readonly ['kici:os:', 'kici:arch:', 'kici:host:', "kici:runtime:"];
224
286
  /**
225
287
  * True if a label is a self-reported platform fact (os/arch/host) rather than a
226
288
  * scaler-assigned, authorization-bearing label. Used by the agent register-time
package/dist/labels.js CHANGED
@@ -253,6 +253,39 @@ function capabilityLabel(name) {
253
253
  */
254
254
  const SSH_TRANSPORT_CAPABILITY = "kici:capability:ssh-transport";
255
255
  /**
256
+ * Runtime-fact label prefix — `kici:runtime:<name>` states something the AGENT
257
+ * discovered about its own host.
258
+ *
259
+ * Deliberately NOT `kici:capability:`. That prefix grants a privilege
260
+ * (`ssh-transport` custodies the bootstrap SSH key), so it must stay
261
+ * token-bound and an agent must never be able to self-assert it. A runtime fact
262
+ * grants nothing: it reports what the host can do, the way `kici:os:` reports
263
+ * what the host is, and it is self-reported for the same reason — the scaler
264
+ * cannot predict it at token-mint time.
265
+ */
266
+ const RUNTIME_LABEL_PREFIX = "kici:runtime:";
267
+ /** Build a `kici:runtime:<name>` label. */
268
+ function runtimeLabel(name) {
269
+ return `${RUNTIME_LABEL_PREFIX}${name}`;
270
+ }
271
+ /**
272
+ * Runtime facts an agent reports about its own host at registration.
273
+ *
274
+ * - `docker` / `podman` — that container runtime's socket answered, so the
275
+ * agent can nest a job container.
276
+ * - `container-build` — a `docker` / `podman` CLI is on PATH, so the agent can
277
+ * build a job's image from a Dockerfile. Distinct from the two above: a host
278
+ * reachable only through a mounted socket can RUN containers but not BUILD
279
+ * one, because the build shells out to the CLI.
280
+ */
281
+ const RuntimeFact = z.enum([
282
+ "docker",
283
+ "podman",
284
+ "container-build"
285
+ ]);
286
+ /** The label a job's `runsOn` names to require a host that can build an image. */
287
+ const CONTAINER_BUILD_RUNTIME_LABEL = runtimeLabel(RuntimeFact.enum["container-build"]);
288
+ /**
256
289
  * The `kici:init` lifecycle label carried by a temporary init-runner agent
257
290
  * brought up on a fresh box for bootstrap. Marks it as ephemeral + privileged;
258
291
  * it dies on reboot and is reaped. Distinct from `kici:role:init-runner`
@@ -274,6 +307,14 @@ function roleToLabel(role) {
274
307
  return `${ROLE_LABEL_PREFIX}${role}`;
275
308
  }
276
309
  /**
310
+ * The label every pre-run evaluation job routes to.
311
+ *
312
+ * Exported because three dispatch sites and the global eval round all target
313
+ * it, and each had spelled the string out. Derived from {@link roleToLabel} so
314
+ * it cannot drift from the prefix or from `KNOWN_ROLES`.
315
+ */
316
+ const INIT_RUNNER_ROLE_LABEL = roleToLabel("init-runner");
317
+ /**
277
318
  * Resolve a roles configuration into role labels.
278
319
  *
279
320
  * - undefined → all roles (backward compat: existing agents get all capabilities)
@@ -289,32 +330,56 @@ function resolveRoleLabels(roles) {
289
330
  }
290
331
  /**
291
332
  * The full label set a scaler injects into an agent it spawns: the base label
292
- * set plus the scaler-assigned `kici:agent:`, `kici:scaler:`, and `kici:role:`
293
- * labels. This is exactly what the scaler writes into `KICI_LABELS`, and it is
294
- * the set an ephemeral agent token must be bound to — the agent then adds only
295
- * the self-reported platform facts (`kici:os:`, `kici:arch:`, `kici:host:`) at
296
- * registration, which the orchestrator's register-time scope gate exempts (see
297
- * `isSelfReportedLabel`). Keep this aligned with the agent's wire-label
298
- * construction in `orchestrator-client.ts#sendAgentRegister`.
299
- */
300
- function scalerAgentLabels(labelSet, backendType, backendName, roles) {
301
- return [
333
+ * set plus the scaler-assigned `kici:agent:`, `kici:scaler:`, `kici:role:`, and
334
+ * platform-taint labels. This is exactly what the scaler writes into
335
+ * `KICI_LABELS`, and it is the set an ephemeral agent token must be bound to —
336
+ * the agent then adds only the self-reported platform facts (`kici:os:`,
337
+ * `kici:arch:`, `kici:host:`) at registration, which the orchestrator's
338
+ * register-time scope gate exempts (see `isSelfReportedLabel`). Keep this
339
+ * aligned with the agent's wire-label construction in
340
+ * `orchestrator-client.ts#sendAgentRegister`.
341
+ *
342
+ * `platformTaints` are the plain tokens (`windows`, `macos`, `arm64`) derived
343
+ * from the pool's resolved platform. A taint is a routing GRANT, not a host
344
+ * fact: it is what a job's `runsOn` names to reach this pool, and the matcher
345
+ * gates on it. So the orchestrator asserts it here — bound into the agent's
346
+ * ephemeral token — rather than letting the agent self-report it. Adding these
347
+ * prefixes to `SELF_REPORTED_LABEL_PREFIXES` instead would let any agent claim
348
+ * any platform unchallenged, which is the opposite of a gate. The caller must
349
+ * derive them from the same source the gate uses
350
+ * (`ScalerManager.resolveScalerPlatform`) — deriving them twice is exactly the
351
+ * defect this parameter closes.
352
+ *
353
+ * The result is de-duplicated in first-seen order: a warm pool measures its
354
+ * readiness with a label set that already carries the taints, so it passes them
355
+ * as `labelSet` AND as `platformTaints`, and a label repeated in the set an
356
+ * ephemeral token is bound to is never meaningful.
357
+ */
358
+ function scalerAgentLabels(labelSet, backendType, backendName, roles, platformTaints = []) {
359
+ return [.../* @__PURE__ */ new Set([
302
360
  ...labelSet,
303
361
  agentTypeLabel(backendType),
304
362
  scalerLabel(backendName),
305
- ...resolveRoleLabels(roles)
306
- ];
363
+ ...resolveRoleLabels(roles),
364
+ ...platformTaints
365
+ ])];
307
366
  }
308
367
  /**
309
368
  * Label-category prefixes the agent self-reports at registration from its own
310
- * host (operating system, CPU architecture, hostname). They are immutable
311
- * platform facts, not authorization grants, so the orchestrator's register-time
312
- * label-scope gate does not require an ephemeral token to be bound to them.
369
+ * host: operating system, CPU architecture, hostname, and runtime facts. They
370
+ * are platform facts, not authorization grants, so the orchestrator's
371
+ * register-time label-scope gate does not require an ephemeral token to be
372
+ * bound to them.
373
+ *
374
+ * Adding a prefix here lets any agent assert it unchallenged. That is correct
375
+ * for a fact about the host and WRONG for anything that grants a privilege —
376
+ * `kici:capability:` is absent from this list for exactly that reason.
313
377
  */
314
378
  const SELF_REPORTED_LABEL_PREFIXES = [
315
379
  "kici:os:",
316
380
  "kici:arch:",
317
- "kici:host:"
381
+ "kici:host:",
382
+ RUNTIME_LABEL_PREFIX
318
383
  ];
319
384
  /**
320
385
  * True if a label is a self-reported platform fact (os/arch/host) rather than a
@@ -352,6 +417,6 @@ function separateLabels(labels) {
352
417
  };
353
418
  }
354
419
  //#endregion
355
- export { CAPABILITY_LABEL_PREFIX, HOST_LABEL_PREFIX, INIT_LABEL, KNOWN_ROLES, PLATFORM_TAINT_LABELS, PRIVILEGED_ROOT_LABEL, RESERVED_LABEL_PREFIX, ROLE_LABEL_PREFIX, SELF_REPORTED_LABEL_PREFIXES, SSH_TRANSPORT_CAPABILITY, ScalerArch, ScalerOs, agentTypeLabel, capabilityLabel, deriveOsArchLabels, derivePlatformTaints, hostLabel, hostToScalerPlatform, isAutoLabel, isSelfReportedLabel, mergeAutoLabels, nodeArchToScalerArch, nodePlatformToScalerOs, normalizeRunsOn, parseHostLabel, platformToOsArchLabels, platformToTaints, resolveRoleLabels, roleToLabel, scalerAgentLabels, scalerLabel, scalerPlatformSchema, separateLabels, validateNoReservedLabels };
420
+ export { CAPABILITY_LABEL_PREFIX, CONTAINER_BUILD_RUNTIME_LABEL, HOST_LABEL_PREFIX, INIT_LABEL, INIT_RUNNER_ROLE_LABEL, KNOWN_ROLES, PLATFORM_TAINT_LABELS, PRIVILEGED_ROOT_LABEL, RESERVED_LABEL_PREFIX, ROLE_LABEL_PREFIX, RUNTIME_LABEL_PREFIX, RuntimeFact, SELF_REPORTED_LABEL_PREFIXES, SSH_TRANSPORT_CAPABILITY, ScalerArch, ScalerOs, agentTypeLabel, capabilityLabel, deriveOsArchLabels, derivePlatformTaints, hostLabel, hostToScalerPlatform, isAutoLabel, isSelfReportedLabel, mergeAutoLabels, nodeArchToScalerArch, nodePlatformToScalerOs, normalizeRunsOn, parseHostLabel, platformToOsArchLabels, platformToTaints, resolveRoleLabels, roleToLabel, runtimeLabel, scalerAgentLabels, scalerLabel, scalerPlatformSchema, separateLabels, validateNoReservedLabels };
356
421
 
357
422
  //# sourceMappingURL=labels.js.map
@@ -15,6 +15,17 @@ export interface HeldRunSummary {
15
15
  * orchestrator's response.
16
16
  */
17
17
  holdType?: string;
18
+ /**
19
+ * Bound context that requested the hold, when the row carries one. Null once
20
+ * the context is deleted, and absent from an older orchestrator's response.
21
+ */
22
+ contextName?: string | null;
23
+ /** Persisted `held_runs.queue_type` (`context` / `security`), when present. */
24
+ queueType?: string;
25
+ /** Why the hold was requested, when the row carries a reason. */
26
+ reason?: string | null;
27
+ /** When the hold expires, as an ISO timestamp; null when it never does. */
28
+ expiresAt?: string | null;
18
29
  /** Computed drift payload for a `when: 'drift'` step hold; absent otherwise. */
19
30
  payload?: {
20
31
  summaryMarkdown: string;
@@ -27,6 +38,26 @@ export interface HeldRunFilter {
27
38
  job?: string;
28
39
  /** Match a step-scoped hold by its step index (compared as a string). */
29
40
  step?: string;
41
+ /**
42
+ * Match one hold by its own id, ignoring every other filter.
43
+ *
44
+ * The escape hatch for an ambiguity nothing else can resolve. A job can carry
45
+ * more than one pending hold — an SDK `requireApproval` and a security-typed
46
+ * context gate produce two job-scoped rows under one job name — and while
47
+ * {@link holdType} separates that pair, nothing guarantees a future pair
48
+ * differs in type. An id always does, and the candidate list prints the ids
49
+ * whenever two holds are otherwise indistinguishable.
50
+ */
51
+ holdId?: string;
52
+ /**
53
+ * Narrow to holds of one type (`reviewer` / `timer` / `concurrency` /
54
+ * `security`), normalized through `normalizePersistedHoldType` on both sides
55
+ * so a legacy `approval` row answers to `reviewer`.
56
+ *
57
+ * Composes with {@link job} / {@link step} rather than replacing them: a
58
+ * matrix whose children each carry two holds needs both halves.
59
+ */
60
+ holdType?: string;
30
61
  }
31
62
  /** Resolution result: either a held-run id or a user-facing error message. */
32
63
  export type ResolveResult = {
@@ -40,10 +71,19 @@ export type ResolveResult = {
40
71
  /**
41
72
  * Resolve the held-run id matching the filter from a list of pending holds.
42
73
  *
74
+ * - `holdId` names one hold outright and ignores every other filter.
43
75
  * - `step` requires `job` and matches a `step`-scoped hold whose step index
44
76
  * equals the given value.
45
77
  * - `job` alone matches a `job`/`workflow`-scoped hold for that job.
78
+ * - `holdType` narrows whichever set the above produced, and may be used on its
79
+ * own.
46
80
  * - With no filter, the sole pending hold is used; ambiguity is an error.
81
+ *
82
+ * One job can carry more than one pending hold: an SDK `requireApproval` paired
83
+ * with a security-typed context gate writes two job-scoped rows under one job
84
+ * name, because two independent requirements gate the job and both must be
85
+ * answered. `job` alone cannot separate those, which is what `holdType` and
86
+ * `holdId` are for.
47
87
  */
48
88
  export declare function resolveHeldRunId(holds: readonly HeldRunSummary[], filter: HeldRunFilter): ResolveResult;
49
89
  //# sourceMappingURL=held-run-resolve.d.ts.map
@@ -1,38 +1,86 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { HeldRunStatus } from "../context/held-run-status.js";
3
+ import { normalizePersistedHoldType } from "../context/hold-type.js";
3
4
  //#region src/mcp/held-run-resolve.ts
4
5
  /**
5
6
  * Shared held-run resolution for the `kici approve` / `kici reject` commands and
6
7
  * the developer MCP `approve_run` / `reject_run` tools.
7
8
  *
8
- * Both surfaces first list the pending holds for a run, then resolve the one
9
- * the caller named via `job` / `step` (or the sole pending hold when there is
10
- * exactly one and no filter is given). The resolution is a pure function so it
11
- * can be unit-tested without HTTP, and it imports nothing beyond the shared
12
- * held-run vocabulary, so it is safe to re-export from the browser-facing
13
- * engine barrel.
9
+ * Both surfaces first list the pending holds for a run, then resolve the one the
10
+ * caller named via `job` / `step` / `holdType` / `holdId` (or the sole pending
11
+ * hold when there is exactly one and no filter is given). The resolution is a
12
+ * pure function so it can be unit-tested without HTTP, and it imports nothing
13
+ * beyond the shared held-run vocabulary, so it is safe to re-export from the
14
+ * browser-facing engine barrel.
15
+ *
16
+ * A job name is not a unique key: two independent requirements can gate one job,
17
+ * and each writes its own pending row under that name. So every ambiguity path
18
+ * here has to end at a filter that can actually separate them, and the candidate
19
+ * list has to print enough for the caller to pick one.
14
20
  */
15
21
  /**
16
22
  * Name a hold the way the caller would type it: the literal `jobId` (so an
17
23
  * `__install__…` sentinel or an opaque legacy id is reproduced verbatim), with
18
- * the step index prefixed for a step-scoped hold.
24
+ * the step index prefixed for a step-scoped hold, and the hold type appended.
25
+ *
26
+ * The type is what makes the candidate list actionable when one job carries two
27
+ * holds — without it both rows rendered as the bare job name, so the list
28
+ * collapsed to a single entry and named a `--job` filter that could not tell
29
+ * them apart. It is omitted when the orchestrator's list response carried none
30
+ * (an older build), which renders exactly as it did before.
19
31
  */
20
32
  function describeHold(hold) {
21
33
  const job = hold.jobId || "(unnamed hold)";
22
- if (hold.holdScope === "step" && hold.stepIndex != null) return `step ${hold.stepIndex} of ${job}`;
23
- return job;
34
+ const base = hold.holdScope === "step" && hold.stepIndex != null ? `step ${hold.stepIndex} of ${job}` : job;
35
+ return hold.holdType ? `${base} (${normalizePersistedHoldType(hold.holdType)})` : base;
24
36
  }
25
- /** The distinct hold descriptions, in listing order, for an error message. */
37
+ /**
38
+ * The hold descriptions, in listing order, for an error message.
39
+ *
40
+ * A description that occurs once is listed once. One that occurs more than once
41
+ * is listed per hold WITH its id, because at that point the description alone
42
+ * cannot name which hold the caller means and `--hold <id>` is the only filter
43
+ * left that can. Silently de-duplicating those is what made the old message
44
+ * offer a disambiguator it had already discarded the information for.
45
+ */
26
46
  function listCandidates(holds) {
27
- return [...new Set(holds.map(describeHold))].join(", ");
47
+ const counts = /* @__PURE__ */ new Map();
48
+ for (const hold of holds) {
49
+ const text = describeHold(hold);
50
+ counts.set(text, (counts.get(text) ?? 0) + 1);
51
+ }
52
+ const emitted = /* @__PURE__ */ new Set();
53
+ const out = [];
54
+ for (const hold of holds) {
55
+ const text = describeHold(hold);
56
+ if ((counts.get(text) ?? 0) > 1) {
57
+ out.push(`${text} [${hold.id}]`);
58
+ continue;
59
+ }
60
+ if (emitted.has(text)) continue;
61
+ emitted.add(text);
62
+ out.push(text);
63
+ }
64
+ return out.join(", ");
28
65
  }
66
+ /** The disambiguators a caller can reach for, named the way the CLI spells them. */
67
+ const DISAMBIGUATORS = "--job <name>, --step <index>, --hold-type <type> or --hold <id>";
29
68
  /**
30
69
  * Resolve the held-run id matching the filter from a list of pending holds.
31
70
  *
71
+ * - `holdId` names one hold outright and ignores every other filter.
32
72
  * - `step` requires `job` and matches a `step`-scoped hold whose step index
33
73
  * equals the given value.
34
74
  * - `job` alone matches a `job`/`workflow`-scoped hold for that job.
75
+ * - `holdType` narrows whichever set the above produced, and may be used on its
76
+ * own.
35
77
  * - With no filter, the sole pending hold is used; ambiguity is an error.
78
+ *
79
+ * One job can carry more than one pending hold: an SDK `requireApproval` paired
80
+ * with a security-typed context gate writes two job-scoped rows under one job
81
+ * name, because two independent requirements gate the job and both must be
82
+ * answered. `job` alone cannot separate those, which is what `holdType` and
83
+ * `holdId` are for.
36
84
  */
37
85
  function resolveHeldRunId(holds, filter) {
38
86
  const pending = holds.filter((h) => h.status === HeldRunStatus.enum.pending);
@@ -40,17 +88,22 @@ function resolveHeldRunId(holds, filter) {
40
88
  ok: false,
41
89
  error: "No pending approval holds found for this run."
42
90
  };
91
+ if (filter.holdId !== void 0) return pickSingle(pending.filter((h) => h.id === filter.holdId), `hold '${filter.holdId}'`, pending);
92
+ const wanted = filter.holdType && normalizePersistedHoldType(filter.holdType);
93
+ const byType = (candidates) => wanted === void 0 ? candidates : candidates.filter((h) => h.holdType && normalizePersistedHoldType(h.holdType) === wanted);
94
+ const typeLabel = wanted ? ` of type '${wanted}'` : "";
43
95
  if (filter.step !== void 0) {
44
96
  if (!filter.job) return {
45
97
  ok: false,
46
98
  error: "--step requires --job to identify the held step."
47
99
  };
48
- return pickSingle(pending.filter((h) => h.holdScope === "step" && h.jobId === filter.job && String(h.stepIndex ?? "") === filter.step), `step ${filter.step} of job '${filter.job}'`, pending);
100
+ return pickSingle(byType(pending.filter((h) => h.holdScope === "step" && h.jobId === filter.job && String(h.stepIndex ?? "") === filter.step)), `step ${filter.step} of job '${filter.job}'${typeLabel}`, pending);
49
101
  }
50
- if (filter.job !== void 0) return pickSingle(pending.filter((h) => h.jobId === filter.job && h.holdScope !== "step"), `job '${filter.job}'`, pending);
102
+ if (filter.job !== void 0) return pickSingle(byType(pending.filter((h) => h.jobId === filter.job && h.holdScope !== "step")), `job '${filter.job}'${typeLabel}`, pending);
103
+ if (wanted !== void 0) return pickSingle(byType(pending), `hold type '${wanted}'`, pending);
51
104
  if (pending.length > 1) return {
52
105
  ok: false,
53
- error: `Multiple pending holds for this run. Use --job <name> (and --step <index>) to choose one. Candidates: ${listCandidates(pending)}.`
106
+ error: `Multiple pending holds for this run. Use ${DISAMBIGUATORS} to choose one. Candidates: ${listCandidates(pending)}.`
54
107
  };
55
108
  return {
56
109
  ok: true,
@@ -65,7 +118,7 @@ function pickSingle(matches, label, pending) {
65
118
  };
66
119
  if (matches.length > 1) return {
67
120
  ok: false,
68
- error: `Multiple pending holds match ${label}; cannot disambiguate. Candidates: ${listCandidates(matches)}.`
121
+ error: `Multiple pending holds match ${label}. Narrow it with ${DISAMBIGUATORS}. Candidates: ${listCandidates(matches)}.`
69
122
  };
70
123
  return {
71
124
  ok: true,
@@ -59,12 +59,16 @@ export declare const approveRunToolSchema: {
59
59
  runId: z.ZodString;
60
60
  job: z.ZodOptional<z.ZodString>;
61
61
  step: z.ZodOptional<z.ZodString>;
62
+ holdType: z.ZodOptional<z.ZodString>;
63
+ holdId: z.ZodOptional<z.ZodString>;
62
64
  };
63
65
  export declare const rejectRunToolSchema: {
64
66
  orgId: z.ZodOptional<z.ZodString>;
65
67
  runId: z.ZodString;
66
68
  job: z.ZodOptional<z.ZodString>;
67
69
  step: z.ZodOptional<z.ZodString>;
70
+ holdType: z.ZodOptional<z.ZodString>;
71
+ holdId: z.ZodOptional<z.ZodString>;
68
72
  reason: z.ZodString;
69
73
  };
70
74
  export declare const cancelRunsByBranchToolSchema: {
@@ -55,17 +55,29 @@ const triggerRunToolSchema = {
55
55
  orgId: orgIdArg,
56
56
  registrationId: z.string().min(1).describe("The workflow registration id to trigger.")
57
57
  };
58
+ /**
59
+ * The two filters that separate holds a job name cannot. One job can carry more
60
+ * than one pending hold — an SDK `requireApproval` paired with a security-typed
61
+ * context gate writes two job-scoped rows under one job name — so `job` alone is
62
+ * ambiguous for that shape, and the error listing names these instead.
63
+ */
64
+ const holdTypeArg = z.string().optional().describe("Hold type (reviewer, timer, concurrency, security), when one job carries more than one hold.");
65
+ const holdIdArg = z.string().optional().describe("A specific hold id, as printed in the candidate list when nothing else separates them.");
58
66
  const approveRunToolSchema = {
59
67
  orgId: orgIdArg,
60
68
  runId: z.string().min(1).describe("The run whose held approval gate to approve."),
61
69
  job: z.string().optional().describe("Job name, to disambiguate when the run has multiple holds."),
62
- step: z.string().optional().describe("Zero-based step index (requires job) for a step-scoped hold.")
70
+ step: z.string().optional().describe("Zero-based step index (requires job) for a step-scoped hold."),
71
+ holdType: holdTypeArg,
72
+ holdId: holdIdArg
63
73
  };
64
74
  const rejectRunToolSchema = {
65
75
  orgId: orgIdArg,
66
76
  runId: z.string().min(1).describe("The run whose held approval gate to reject."),
67
77
  job: z.string().optional().describe("Job name, to disambiguate when the run has multiple holds."),
68
78
  step: z.string().optional().describe("Zero-based step index (requires job) for a step-scoped hold."),
79
+ holdType: holdTypeArg,
80
+ holdId: holdIdArg,
69
81
  reason: z.string().min(1).describe("Why the gate is rejected (required).")
70
82
  };
71
83
  const cancelRunsByBranchToolSchema = {
@@ -55,10 +55,24 @@ export declare const ORCH_PUSHED_METRIC_NAMES: ReadonlySet<MetricName>;
55
55
  /**
56
56
  * Closed enum of scaler-backend TYPE values the orchestrator's own
57
57
  * resource-usage gauges carry on their `scalerType` rollup label.
58
- * `__global__` is the orchestrator-wide rollup row; the other four match
59
- * `AGENT_SCALER_VALUES`. Exported so the policy tests can assert against it.
58
+ * `__global__` is the orchestrator-wide rollup row; every other value comes
59
+ * from `ScalerManager`'s own `backends` map, so the enum is `ScalerBackendType`
60
+ * itself — NOT `AGENT_SCALER_VALUES`, which additionally carries the
61
+ * agent-only `stateful` and predates the `event` backend. Exported so the
62
+ * policy tests can assert against it.
60
63
  */
61
- export declare const ORCH_SCALER_VALUES: readonly ["__global__", "stateful", "container", "firecracker", "bare-metal"];
64
+ export declare const ORCH_SCALER_VALUES: readonly ["__global__", ...("bare-metal" | "container" | "event" | "firecracker" | "kubernetes")[]];
65
+ /**
66
+ * Closed enum of the scheduled jobs the orchestrator runs. MUST mirror
67
+ * `OrchestratorScheduledJobName` (`packages/orchestrator/src/queue/scheduled-job.ts`)
68
+ * exactly — a job missing here has its whole `kici_orch_job_*` series
69
+ * dropped by the Platform filter with `reason="bad_label_value"`, so the
70
+ * job silently loses every health metric. The engine cannot import the
71
+ * orchestrator (dependency runs the other way), so the parity assertion
72
+ * lives on the orchestrator side in `scheduled-job.test.ts`. Exported so
73
+ * that test can assert against it.
74
+ */
75
+ export declare const ORCH_JOB_VALUES: readonly ['cleanup', 'orphan-secret-cleanup', 'token-cleanup', 'cold-store-archive', 'cold-store-purge', 'unroutable-probe'];
62
76
  /**
63
77
  * Per-metric, per-label value policy. Missing entries (or missing label
64
78
  * keys within an entry) mean "no value-level constraint" — the label key
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { ExecutionRunStatus } from "../protocol/messages/execution-status.js";
3
+ import { ScalerBackendType } from "../scaler/scaler-backend-type.js";
3
4
  import { MetricNames, MetricService } from "./metric-catalog.generated.js";
4
5
  //#region src/metrics/catalog-policy.ts
5
6
  /**
@@ -41,27 +42,43 @@ const OVERFLOW_LABEL_VALUE = "__overflow__";
41
42
  * so they are valid push payloads alongside `kici_orch_*`.
42
43
  */
43
44
  const ORCH_PUSHED_METRIC_NAMES = new Set(Object.entries(MetricService).filter(([, svc]) => svc === "orchestrator" || svc === "agent" || svc === "runtime").map(([key]) => MetricNames[key]));
44
- /** Closed enum of the four scaler-backend label values an agent can carry. */
45
- const AGENT_SCALER_VALUES = [
46
- "stateful",
47
- "container",
48
- "firecracker",
49
- "bare-metal"
50
- ];
45
+ /**
46
+ * Closed enum of the `scaler` label values an agent can carry: every scaler
47
+ * backend type the orchestrator can resolve for a managed agent, plus
48
+ * `stateful` for a static agent no scaler manages (the fallback
49
+ * `AgentMetricsAggregator.resolveScalerLabel` stamps when the lookup returns
50
+ * null). Derived from `ScalerBackendType` rather than restated, so adding a
51
+ * backend cannot silently drop every `kici_agent_*` series that backend's
52
+ * agents emit as `bad_label_value`.
53
+ */
54
+ const AGENT_SCALER_VALUES = ["stateful", ...ScalerBackendType.options];
51
55
  /**
52
56
  * Closed enum of scaler-backend TYPE values the orchestrator's own
53
57
  * resource-usage gauges carry on their `scalerType` rollup label.
54
- * `__global__` is the orchestrator-wide rollup row; the other four match
55
- * `AGENT_SCALER_VALUES`. Exported so the policy tests can assert against it.
58
+ * `__global__` is the orchestrator-wide rollup row; every other value comes
59
+ * from `ScalerManager`'s own `backends` map, so the enum is `ScalerBackendType`
60
+ * itself — NOT `AGENT_SCALER_VALUES`, which additionally carries the
61
+ * agent-only `stateful` and predates the `event` backend. Exported so the
62
+ * policy tests can assert against it.
63
+ */
64
+ const ORCH_SCALER_VALUES = ["__global__", ...ScalerBackendType.options];
65
+ /**
66
+ * Closed enum of the scheduled jobs the orchestrator runs. MUST mirror
67
+ * `OrchestratorScheduledJobName` (`packages/orchestrator/src/queue/scheduled-job.ts`)
68
+ * exactly — a job missing here has its whole `kici_orch_job_*` series
69
+ * dropped by the Platform filter with `reason="bad_label_value"`, so the
70
+ * job silently loses every health metric. The engine cannot import the
71
+ * orchestrator (dependency runs the other way), so the parity assertion
72
+ * lives on the orchestrator side in `scheduled-job.test.ts`. Exported so
73
+ * that test can assert against it.
56
74
  */
57
- const ORCH_SCALER_VALUES = ["__global__", ...AGENT_SCALER_VALUES];
58
- /** Closed enum of the five scheduled jobs the orchestrator runs (mirrors `OrchestratorScheduledJobName`). */
59
75
  const ORCH_JOB_VALUES = [
60
76
  "cleanup",
61
77
  "orphan-secret-cleanup",
62
78
  "token-cleanup",
63
79
  "cold-store-archive",
64
- "cold-store-purge"
80
+ "cold-store-purge",
81
+ "unroutable-probe"
65
82
  ];
66
83
  /**
67
84
  * Per-metric, per-label value policy. Missing entries (or missing label
@@ -136,6 +153,20 @@ const METRIC_LABEL_POLICY = {
136
153
  scalerType: { values: ORCH_SCALER_VALUES },
137
154
  machinePool: { maxUniqueValues: 50 }
138
155
  },
156
+ kici_orch_scaler_warm_pool_target: {
157
+ scaler: { maxUniqueValues: 50 },
158
+ labelSet: { maxUniqueValues: 100 }
159
+ },
160
+ kici_orch_scaler_warm_pool_ready: {
161
+ scaler: { maxUniqueValues: 50 },
162
+ labelSet: { maxUniqueValues: 100 }
163
+ },
164
+ kici_orch_scaler_warm_pool_in_flight: {
165
+ scaler: { maxUniqueValues: 50 },
166
+ labelSet: { maxUniqueValues: 100 }
167
+ },
168
+ kici_orch_scaler_warm_pool_spawns_total: { scaler: { maxUniqueValues: 50 } },
169
+ kici_orch_scaler_warm_pool_reaped_total: { scaler: { maxUniqueValues: 50 } },
139
170
  kici_orch_install_secrets_decisions_total: {
140
171
  decision: { values: [
141
172
  "pass",
@@ -277,6 +308,6 @@ const METRIC_LABEL_POLICY = {
277
308
  }
278
309
  };
279
310
  //#endregion
280
- export { METRIC_LABEL_POLICY, ORCH_PUSHED_METRIC_NAMES, ORCH_SCALER_VALUES, OVERFLOW_LABEL_VALUE };
311
+ export { METRIC_LABEL_POLICY, ORCH_JOB_VALUES, ORCH_PUSHED_METRIC_NAMES, ORCH_SCALER_VALUES, OVERFLOW_LABEL_VALUE };
281
312
 
282
313
  //# sourceMappingURL=catalog-policy.js.map