@kici-dev/engine 0.1.22 → 0.1.24

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 (72) hide show
  1. package/dist/approval/types.d.ts +1 -1
  2. package/dist/audit/access-log-policy.d.ts +4 -1
  3. package/dist/audit/access-log-policy.js +11 -1
  4. package/dist/audit/activity.d.ts +3 -1
  5. package/dist/audit/activity.js +8 -16
  6. package/dist/audit/retention-policy.js +6 -0
  7. package/dist/environment/host-match.d.ts +25 -0
  8. package/dist/environment/host-match.js +67 -0
  9. package/dist/environment/index.d.ts +3 -0
  10. package/dist/environment/index.js +3 -1
  11. package/dist/environment/multi-env.d.ts +30 -0
  12. package/dist/environment/multi-env.js +38 -0
  13. package/dist/environment/scope-resolver.d.ts +15 -4
  14. package/dist/environment/scope-resolver.js +55 -15
  15. package/dist/environment/scope-template.d.ts +18 -0
  16. package/dist/environment/scope-template.js +43 -0
  17. package/dist/environment/types.d.ts +15 -2
  18. package/dist/environment/types.js +18 -1
  19. package/dist/fanout/materialize.d.ts +34 -0
  20. package/dist/fanout/materialize.js +72 -45
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.js +21 -10
  23. package/dist/inputs/build.d.ts +9 -0
  24. package/dist/inputs/build.js +44 -0
  25. package/dist/inputs/coerce.d.ts +25 -0
  26. package/dist/inputs/coerce.js +51 -0
  27. package/dist/inputs/descriptor.d.ts +53 -0
  28. package/dist/inputs/descriptor.js +42 -0
  29. package/dist/inputs/extract.d.ts +15 -0
  30. package/dist/inputs/extract.js +117 -0
  31. package/dist/inputs/index.d.ts +6 -0
  32. package/dist/inputs/index.js +7 -0
  33. package/dist/inputs/schedule.d.ts +19 -0
  34. package/dist/inputs/schedule.js +33 -0
  35. package/dist/labels/compile.d.ts +9 -0
  36. package/dist/labels/compile.js +10 -1
  37. package/dist/labels.d.ts +29 -0
  38. package/dist/labels.js +32 -1
  39. package/dist/mcp/tool-schemas.d.ts +41 -0
  40. package/dist/mcp/tool-schemas.js +49 -0
  41. package/dist/metrics/catalog-policy.d.ts +7 -0
  42. package/dist/metrics/catalog-policy.js +11 -12
  43. package/dist/metrics/metric-catalog.generated.d.ts +22 -2
  44. package/dist/metrics/metric-catalog.generated.js +34 -2
  45. package/dist/protocol/messages/access-log.d.ts +39 -10
  46. package/dist/protocol/messages/access-log.js +7 -2
  47. package/dist/protocol/messages/actor.d.ts +8 -0
  48. package/dist/protocol/messages/actor.js +17 -3
  49. package/dist/protocol/messages/agent-run-result.d.ts +350 -0
  50. package/dist/protocol/messages/agent-run-result.js +123 -0
  51. package/dist/protocol/messages/auth.d.ts +2 -0
  52. package/dist/protocol/messages/auth.js +10 -1
  53. package/dist/protocol/messages/capabilities.d.ts +2 -0
  54. package/dist/protocol/messages/capabilities.js +15 -2
  55. package/dist/protocol/messages/dashboard-global-workflows.d.ts +8 -0
  56. package/dist/protocol/messages/dashboard.d.ts +1513 -39
  57. package/dist/protocol/messages/dashboard.js +210 -15
  58. package/dist/protocol/messages/execution-status.d.ts +52 -1
  59. package/dist/protocol/messages/execution-status.js +115 -54
  60. package/dist/protocol/messages/orchestrator-agent.d.ts +21 -5
  61. package/dist/protocol/messages/orchestrator-agent.js +5 -1
  62. package/dist/protocol/messages/pat-kind.d.ts +16 -0
  63. package/dist/protocol/messages/pat-kind.js +17 -0
  64. package/dist/protocol/messages/peer.d.ts +7 -7
  65. package/dist/protocol/messages/platform-orchestrator.d.ts +424 -8
  66. package/dist/protocol/messages/platform-orchestrator.js +3 -3
  67. package/dist/protocol/messages/run-events.d.ts +5 -1
  68. package/dist/provider/check-run-conclusion.d.ts +1 -1
  69. package/dist/trigger/types.d.ts +75 -7
  70. package/dist/trigger/types.js +19 -2
  71. package/package.json +9 -1
  72. package/sbom.spdx.json +5 -5
@@ -22,8 +22,8 @@ export type ApproverClause = z.infer<typeof approverClauseSchema>;
22
22
  /** Granularity of a held element. */
23
23
  export declare const HoldScope: z.ZodEnum<{
24
24
  job: "job";
25
- step: "step";
26
25
  workflow: "workflow";
26
+ step: "step";
27
27
  }>;
28
28
  export type HoldScope = z.infer<typeof HoldScope>;
29
29
  /** What triggered the hold: an environment policy (mandatory) or SDK code (explicit). */
@@ -11,7 +11,10 @@
11
11
  * 1. `outcome !== 'allowed'` → always record (denied/error are full-fidelity).
12
12
  * 2. `actor.type === 'platform_operator'` → always record (compliance: any
13
13
  * activity by a non-org member must be auditable in full).
14
- * 3. The per-action `POLICY_BY_ACTION` decision (`always` / `sample` /
14
+ * 3. agent-attributed actor (`actor.agent` present) always record. The
15
+ * agent-provenance guarantee is that every action an agent takes is fully
16
+ * auditable; a sampled subset would break it.
17
+ * 4. The per-action `POLICY_BY_ACTION` decision (`always` / `sample` /
15
18
  * `rate_limit`).
16
19
  *
17
20
  * Browser-safe: this module MUST NOT import any Node built-ins (`node:crypto`
@@ -14,7 +14,10 @@ import { z } from "zod";
14
14
  * 1. `outcome !== 'allowed'` → always record (denied/error are full-fidelity).
15
15
  * 2. `actor.type === 'platform_operator'` → always record (compliance: any
16
16
  * activity by a non-org member must be auditable in full).
17
- * 3. The per-action `POLICY_BY_ACTION` decision (`always` / `sample` /
17
+ * 3. agent-attributed actor (`actor.agent` present) always record. The
18
+ * agent-provenance guarantee is that every action an agent takes is fully
19
+ * auditable; a sampled subset would break it.
20
+ * 4. The per-action `POLICY_BY_ACTION` decision (`always` / `sample` /
18
21
  * `rate_limit`).
19
22
  *
20
23
  * Browser-safe: this module MUST NOT import any Node built-ins (`node:crypto`
@@ -38,6 +41,10 @@ const POLICY_BY_ACTION = {
38
41
  kind: "sample",
39
42
  allowedRate: .05
40
43
  },
44
+ "run.structured.read": {
45
+ kind: "sample",
46
+ allowedRate: .05
47
+ },
41
48
  "runs.list.read": {
42
49
  kind: "sample",
43
50
  allowedRate: .05
@@ -152,6 +159,8 @@ const POLICY_BY_ACTION = {
152
159
  "backend.test": { kind: "always" },
153
160
  "fleet.host.declare": { kind: "always" },
154
161
  "fleet.host.remove": { kind: "always" },
162
+ "fleet.init_runner.bringup": { kind: "always" },
163
+ "fleet.pre_boot.send": { kind: "always" },
155
164
  "global_workflows.update": { kind: "always" },
156
165
  "org_settings.dashboard_write_policy.update": { kind: "always" },
157
166
  "cluster_name.update": { kind: "always" },
@@ -204,6 +213,7 @@ function actorKey(actor) {
204
213
  function shouldRecordAccess(action, outcome, actor, requestId, limiter) {
205
214
  if (outcome !== "allowed") return true;
206
215
  if (actor.type === "platform_operator") return true;
216
+ if (actor.type === "user" && actor.agent) return true;
207
217
  const policy = POLICY_BY_ACTION[action];
208
218
  switch (policy.kind) {
209
219
  case "always": return true;
@@ -16,7 +16,7 @@
16
16
  import { z } from 'zod';
17
17
  /**
18
18
  * Source discriminator on a unified Activity row.
19
- * NOT to be confused with `AccessLogSource` (platform_proxy / admin_http /
19
+ * NOT to be confused with `AccessLogSource` (platform_proxy / admin_http / agent /
20
20
  * admin_cli) — that field is renamed to `origin` on the unified row to
21
21
  * avoid the collision.
22
22
  */
@@ -73,6 +73,7 @@ export declare const activityRowSchema: z.ZodObject<{
73
73
  details: z.ZodNullable<z.ZodUnknown>;
74
74
  requestId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
75
75
  origin: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
76
+ agent: "agent";
76
77
  platform_proxy: "platform_proxy";
77
78
  admin_http: "admin_http";
78
79
  admin_cli: "admin_cli";
@@ -105,6 +106,7 @@ export declare const activityFilterSchema: z.ZodObject<{
105
106
  denied: "denied";
106
107
  }>>;
107
108
  origin: z.ZodOptional<z.ZodEnum<{
109
+ agent: "agent";
108
110
  platform_proxy: "platform_proxy";
109
111
  admin_http: "admin_http";
110
112
  admin_cli: "admin_cli";
@@ -1,6 +1,6 @@
1
1
  import "../chunk-BTugEXQM.js";
2
2
  import { ActorType } from "../protocol/messages/actor.js";
3
- import { AccessLogOutcome } from "../protocol/messages/access-log.js";
3
+ import { AccessLogOutcome, AccessLogSource } from "../protocol/messages/access-log.js";
4
4
  import { z } from "zod";
5
5
  //#region src/audit/activity.ts
6
6
  /**
@@ -20,7 +20,7 @@ import { z } from "zod";
20
20
  */
21
21
  /**
22
22
  * Source discriminator on a unified Activity row.
23
- * NOT to be confused with `AccessLogSource` (platform_proxy / admin_http /
23
+ * NOT to be confused with `AccessLogSource` (platform_proxy / admin_http / agent /
24
24
  * admin_cli) — that field is renamed to `origin` on the unified row to
25
25
  * avoid the collision.
26
26
  */
@@ -71,16 +71,12 @@ const activityRowSchema = z.object({
71
71
  /** access_log only — request correlation ID. */
72
72
  requestId: z.string().nullable().optional(),
73
73
  /**
74
- * access_log only — origin of the entry (platform_proxy / admin_http /
75
- * admin_cli). Renamed from `source` on the access_log row schema so the
76
- * unified row's `source` field is unambiguously the audit_log vs.
77
- * access_log discriminator.
74
+ * access_log only — origin of the entry (the AccessLogSource values:
75
+ * platform_proxy / admin_http / admin_cli / agent). Renamed from `source` on
76
+ * the access_log row schema so the unified row's `source` field is
77
+ * unambiguously the audit_log vs. access_log discriminator.
78
78
  */
79
- origin: z.enum([
80
- "platform_proxy",
81
- "admin_http",
82
- "admin_cli"
83
- ]).nullable().optional()
79
+ origin: AccessLogSource.nullable().optional()
84
80
  });
85
81
  /**
86
82
  * Unified filter schema accepted by GET /orgs/:customerId/activity.
@@ -94,11 +90,7 @@ const activityFilterSchema = z.object({
94
90
  /** Free-form: matches access_log AccessLogAction values OR audit_log free-form actions. */
95
91
  action: z.string().optional(),
96
92
  outcome: AccessLogOutcome.optional(),
97
- origin: z.enum([
98
- "platform_proxy",
99
- "admin_http",
100
- "admin_cli"
101
- ]).optional(),
93
+ origin: AccessLogSource.optional(),
102
94
  targetType: z.string().optional(),
103
95
  targetId: z.string().optional(),
104
96
  /** Sugar: maps to targetType='run' + targetId=runId. Both halves of the federation respect it. */
@@ -13,6 +13,7 @@ import "../chunk-BTugEXQM.js";
13
13
  */
14
14
  const ACCESS_LOG_WARM_DAYS = {
15
15
  "run.detail.read": 30,
16
+ "run.structured.read": 30,
16
17
  "runs.list.read": 30,
17
18
  "runs.filters.read": 30,
18
19
  "sources.list.read": 30,
@@ -65,6 +66,8 @@ const ACCESS_LOG_WARM_DAYS = {
65
66
  "backend.test": 180,
66
67
  "fleet.host.declare": 180,
67
68
  "fleet.host.remove": 180,
69
+ "fleet.init_runner.bringup": 180,
70
+ "fleet.pre_boot.send": 180,
68
71
  "global_workflows.update": 180,
69
72
  "org_settings.dashboard_write_policy.update": 180,
70
73
  "cluster_name.update": 180,
@@ -255,6 +258,7 @@ function secretAuditLogWarmSqlCase() {
255
258
  */
256
259
  const ACCESS_LOG_COLD_DAYS = {
257
260
  "run.detail.read": 180,
261
+ "run.structured.read": 180,
258
262
  "runs.list.read": 180,
259
263
  "runs.filters.read": 180,
260
264
  "sources.list.read": 180,
@@ -307,6 +311,8 @@ const ACCESS_LOG_COLD_DAYS = {
307
311
  "backend.test": 730,
308
312
  "fleet.host.declare": 730,
309
313
  "fleet.host.remove": 730,
314
+ "fleet.init_runner.bringup": 730,
315
+ "fleet.pre_boot.send": 730,
310
316
  "global_workflows.update": 730,
311
317
  "org_settings.dashboard_write_policy.update": 730,
312
318
  "cluster_name.update": 730,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Identity facts of a single fan-out child, matched against a binding's
3
+ * `host_pattern`. `agentId` / `host` are the stable dispatch identity; `labels`
4
+ * is the host's label set. The union of all three is the match target.
5
+ */
6
+ export interface HostFacts {
7
+ agentId: string;
8
+ host: string;
9
+ labels: readonly string[];
10
+ }
11
+ /**
12
+ * Whether a fan-out child's identity facts satisfy a binding's `host_pattern`.
13
+ *
14
+ * `'**'` / empty matches every host. Otherwise the pattern is compiled once
15
+ * (exact / glob / regex, same selector grammar as `runsOnAll`) and tested
16
+ * against the union `[agentId, host, ...labels]` — true if any element matches.
17
+ */
18
+ export declare function matchHostPattern(facts: HostFacts, pattern: string): boolean;
19
+ /**
20
+ * Rank a `host_pattern` by specificity for precedence: an exact literal (2)
21
+ * beats a glob/regex (1), which beats `'**'`/empty (0). Used to let a per-host
22
+ * binding override a fleet-wide one on a key collision.
23
+ */
24
+ export declare function hostSpecificity(pattern: string): number;
25
+ //# sourceMappingURL=host-match.d.ts.map
@@ -0,0 +1,67 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import { matcherMatches } from "../labels-match.js";
3
+ import { assertSafeRegex, toLabelMatcher } from "../labels/compile.js";
4
+ //#region src/environment/host-match.ts
5
+ /** `'**'`, empty, or absent means "matches every host". */
6
+ function matchesAllHosts(pattern) {
7
+ return pattern === "" || pattern === "**";
8
+ }
9
+ /** True when `pattern` is the `/source/flags` regex-string convention. */
10
+ function asRegexString(pattern) {
11
+ if (pattern.length < 2 || !pattern.startsWith("/")) return null;
12
+ const lastSlash = pattern.lastIndexOf("/");
13
+ if (lastSlash === 0) return null;
14
+ return {
15
+ source: pattern.slice(1, lastSlash),
16
+ flags: pattern.slice(lastSlash + 1)
17
+ };
18
+ }
19
+ /**
20
+ * Compile a `host_pattern` string into a `LabelMatcher`.
21
+ *
22
+ * - `/source/flags` → regex matcher (ReDoS-gated).
23
+ * - a glob (picomatch-detected) → regex matcher.
24
+ * - any other string → exact matcher.
25
+ */
26
+ function compileHostPattern(pattern) {
27
+ const re = asRegexString(pattern);
28
+ if (re) {
29
+ assertSafeRegex(re.source, re.flags, `host_pattern /${re.source}/${re.flags}`);
30
+ return {
31
+ kind: "regex",
32
+ source: re.source,
33
+ flags: re.flags
34
+ };
35
+ }
36
+ return toLabelMatcher(pattern, `host_pattern '${pattern}'`);
37
+ }
38
+ /**
39
+ * Whether a fan-out child's identity facts satisfy a binding's `host_pattern`.
40
+ *
41
+ * `'**'` / empty matches every host. Otherwise the pattern is compiled once
42
+ * (exact / glob / regex, same selector grammar as `runsOnAll`) and tested
43
+ * against the union `[agentId, host, ...labels]` — true if any element matches.
44
+ */
45
+ function matchHostPattern(facts, pattern) {
46
+ if (matchesAllHosts(pattern)) return true;
47
+ const matcher = compileHostPattern(pattern);
48
+ return [
49
+ facts.agentId,
50
+ facts.host,
51
+ ...facts.labels
52
+ ].some((c) => matcherMatches(matcher, c));
53
+ }
54
+ /**
55
+ * Rank a `host_pattern` by specificity for precedence: an exact literal (2)
56
+ * beats a glob/regex (1), which beats `'**'`/empty (0). Used to let a per-host
57
+ * binding override a fleet-wide one on a key collision.
58
+ */
59
+ function hostSpecificity(pattern) {
60
+ if (matchesAllHosts(pattern)) return 0;
61
+ if (asRegexString(pattern)) return 1;
62
+ return toLabelMatcher(pattern, `host_pattern '${pattern}'`).kind === "regex" ? 1 : 2;
63
+ }
64
+ //#endregion
65
+ export { hostSpecificity, matchHostPattern };
66
+
67
+ //# sourceMappingURL=host-match.js.map
@@ -1,3 +1,6 @@
1
1
  export type { Environment, EnvironmentBinding, EnvironmentVariable, EnvironmentSourceOverride, ScopedSecret, HeldRun, ProtectionGateResult, TrustTier, } from './types.js';
2
+ export { TrustTierSchema } from './types.js';
2
3
  export { resolveSecretsForEnvironment, matchScopePattern, stripScopePrefix, } from './scope-resolver.js';
4
+ export type { HostFacts } from './host-match.js';
5
+ export { mergeOrderedMaps, EnvGateRejectReason } from './multi-env.js';
3
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,5 @@
1
1
  import "../chunk-BTugEXQM.js";
2
+ import { TrustTierSchema } from "./types.js";
2
3
  import { matchScopePattern, resolveSecretsForEnvironment, stripScopePrefix } from "./scope-resolver.js";
3
- export { matchScopePattern, resolveSecretsForEnvironment, stripScopePrefix };
4
+ import { EnvGateRejectReason, mergeOrderedMaps } from "./multi-env.js";
5
+ export { EnvGateRejectReason, TrustTierSchema, matchScopePattern, mergeOrderedMaps, resolveSecretsForEnvironment, stripScopePrefix };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Multi-environment merge helpers shared across the orchestrator dispatch path.
3
+ *
4
+ * A job may bind several deployment environments in order; their resolved
5
+ * secrets/variables are folded last-wins (a later environment's key overrides an
6
+ * earlier one) and their protection gates combine all-must-pass. This module is
7
+ * browser-safe (pure functions + a Zod enum, no Node built-ins) so it can be
8
+ * exported from the engine environment barrel.
9
+ */
10
+ import { z } from 'zod';
11
+ /**
12
+ * Reason a single bound environment rejects a run under all-must-pass
13
+ * aggregation. Used to name which environment and which rule blocked dispatch.
14
+ */
15
+ export declare const EnvGateRejectReason: z.ZodEnum<{
16
+ branch_restricted: "branch_restricted";
17
+ trigger_filtered: "trigger_filtered";
18
+ repo_unmatched: "repo_unmatched";
19
+ trust_too_low: "trust_too_low";
20
+ env_disabled: "env_disabled";
21
+ env_not_found: "env_not_found";
22
+ }>;
23
+ export type EnvGateRejectReason = z.infer<typeof EnvGateRejectReason>;
24
+ /**
25
+ * Fold an ordered list of per-environment resolved maps into one. Later entries
26
+ * override earlier keys (last-wins), matching the documented merge order for
27
+ * `environments: [...]`.
28
+ */
29
+ export declare function mergeOrderedMaps(maps: ReadonlyArray<Record<string, string>>): Record<string, string>;
30
+ //# sourceMappingURL=multi-env.d.ts.map
@@ -0,0 +1,38 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import { z } from "zod";
3
+ //#region src/environment/multi-env.ts
4
+ /**
5
+ * Multi-environment merge helpers shared across the orchestrator dispatch path.
6
+ *
7
+ * A job may bind several deployment environments in order; their resolved
8
+ * secrets/variables are folded last-wins (a later environment's key overrides an
9
+ * earlier one) and their protection gates combine all-must-pass. This module is
10
+ * browser-safe (pure functions + a Zod enum, no Node built-ins) so it can be
11
+ * exported from the engine environment barrel.
12
+ */
13
+ /**
14
+ * Reason a single bound environment rejects a run under all-must-pass
15
+ * aggregation. Used to name which environment and which rule blocked dispatch.
16
+ */
17
+ const EnvGateRejectReason = z.enum([
18
+ "branch_restricted",
19
+ "trigger_filtered",
20
+ "repo_unmatched",
21
+ "trust_too_low",
22
+ "env_disabled",
23
+ "env_not_found"
24
+ ]);
25
+ /**
26
+ * Fold an ordered list of per-environment resolved maps into one. Later entries
27
+ * override earlier keys (last-wins), matching the documented merge order for
28
+ * `environments: [...]`.
29
+ */
30
+ function mergeOrderedMaps(maps) {
31
+ const out = {};
32
+ for (const m of maps) Object.assign(out, m);
33
+ return out;
34
+ }
35
+ //#endregion
36
+ export { EnvGateRejectReason, mergeOrderedMaps };
37
+
38
+ //# sourceMappingURL=multi-env.js.map
@@ -1,4 +1,6 @@
1
1
  import type { EnvironmentBinding, ScopedSecret } from './types.js';
2
+ import { type HostFacts } from './host-match.js';
3
+ export type { HostFacts } from './host-match.js';
2
4
  /**
3
5
  * Check whether a scope string matches a pattern.
4
6
  *
@@ -22,14 +24,23 @@ export declare function stripScopePrefix(scope: string): string;
22
24
  * Resolve secrets for an environment by matching bindings against scoped secrets.
23
25
  *
24
26
  * Takes already-filtered bindings (for the target environment). For each binding,
25
- * finds secrets whose scope matches the binding's scopePattern via picomatch.
26
- * When multiple scopes provide the same key, longest scope path wins (higher specificity).
27
- * Scope depth is computed AFTER stripping the backend prefix.
27
+ * finds secrets whose (substituted) scope matches the binding's scopePattern via
28
+ * picomatch. When multiple scopes provide the same key, precedence is the tuple
29
+ * `(host specificity, scope depth)` a per-host binding (exact host) overrides a
30
+ * fleet-wide one, then longest scope path wins. Scope depth is computed AFTER
31
+ * stripping the backend prefix.
32
+ *
33
+ * When `hostFacts` is supplied (a fan-out child's identity), each binding is
34
+ * gated by its `host_pattern` and its `scope_pattern` is templated per-child
35
+ * (`${agentId}`/`${host}`/`${label:NAME}`). When omitted, only fleet-wide
36
+ * (`'**'`/NULL) non-templated bindings contribute — preserving the workflow-level
37
+ * (no-host) behaviour.
28
38
  *
29
39
  * @param bindings - Bindings already filtered for the target environment
30
40
  * @param allSecrets - All scoped secrets in the org
31
41
  * @param decryptFn - Pure decryption function (keeps this module crypto-free)
42
+ * @param hostFacts - Optional fan-out child identity for per-host resolution
32
43
  * @returns Flat record of decrypted secret key-value pairs
33
44
  */
34
- export declare function resolveSecretsForEnvironment(bindings: EnvironmentBinding[], allSecrets: ScopedSecret[], decryptFn: (s: ScopedSecret) => string): Record<string, string>;
45
+ export declare function resolveSecretsForEnvironment(bindings: EnvironmentBinding[], allSecrets: ScopedSecret[], decryptFn: (s: ScopedSecret) => string, hostFacts?: HostFacts): Record<string, string>;
35
46
  //# sourceMappingURL=scope-resolver.d.ts.map
@@ -1,4 +1,6 @@
1
1
  import "../chunk-BTugEXQM.js";
2
+ import { hostSpecificity, matchHostPattern } from "./host-match.js";
3
+ import { substituteScopePattern } from "./scope-template.js";
2
4
  import picomatch from "picomatch";
3
5
  //#region src/environment/scope-resolver.ts
4
6
  /**
@@ -41,31 +43,69 @@ function stripScopePrefix(scope) {
41
43
  return colonIdx >= 0 ? scope.slice(colonIdx + 1) : scope;
42
44
  }
43
45
  /**
46
+ * Resolve the effective scope pattern a binding contributes for a given host,
47
+ * applying the host gate and per-child scope-pattern templating.
48
+ *
49
+ * Returns the (possibly substituted) scope pattern to glob-match secrets
50
+ * against, or `null` when the binding contributes nothing for this host:
51
+ * - With `hostFacts`: skipped when `host_pattern` doesn't match, or when a
52
+ * templated `scope_pattern` can't be substituted (missing label / unsafe
53
+ * value).
54
+ * - Without `hostFacts` (workflow-level, no-host caller): only `'**'`/NULL
55
+ * host bindings contribute, and a templated `scope_pattern` is skipped (it
56
+ * cannot be substituted without facts).
57
+ */
58
+ function bindingScopeForHost(binding, hostFacts) {
59
+ if (hostFacts) {
60
+ if (!matchHostPattern(hostFacts, binding.hostPattern)) return null;
61
+ return substituteScopePattern(binding.scopePattern, hostFacts);
62
+ }
63
+ if (binding.hostPattern !== "**" && binding.hostPattern !== "") return null;
64
+ return binding.scopePattern.includes("${") ? null : binding.scopePattern;
65
+ }
66
+ /** Higher `(hostSpec, scopeDepth)` wins; ties keep the first-encountered. */
67
+ function candidateWins(c, existing) {
68
+ if (!existing) return true;
69
+ if (c.hostSpec !== existing.hostSpec) return c.hostSpec > existing.hostSpec;
70
+ return c.scopeDepth > existing.scopeDepth;
71
+ }
72
+ /**
44
73
  * Resolve secrets for an environment by matching bindings against scoped secrets.
45
74
  *
46
75
  * Takes already-filtered bindings (for the target environment). For each binding,
47
- * finds secrets whose scope matches the binding's scopePattern via picomatch.
48
- * When multiple scopes provide the same key, longest scope path wins (higher specificity).
49
- * Scope depth is computed AFTER stripping the backend prefix.
76
+ * finds secrets whose (substituted) scope matches the binding's scopePattern via
77
+ * picomatch. When multiple scopes provide the same key, precedence is the tuple
78
+ * `(host specificity, scope depth)` a per-host binding (exact host) overrides a
79
+ * fleet-wide one, then longest scope path wins. Scope depth is computed AFTER
80
+ * stripping the backend prefix.
81
+ *
82
+ * When `hostFacts` is supplied (a fan-out child's identity), each binding is
83
+ * gated by its `host_pattern` and its `scope_pattern` is templated per-child
84
+ * (`${agentId}`/`${host}`/`${label:NAME}`). When omitted, only fleet-wide
85
+ * (`'**'`/NULL) non-templated bindings contribute — preserving the workflow-level
86
+ * (no-host) behaviour.
50
87
  *
51
88
  * @param bindings - Bindings already filtered for the target environment
52
89
  * @param allSecrets - All scoped secrets in the org
53
90
  * @param decryptFn - Pure decryption function (keeps this module crypto-free)
91
+ * @param hostFacts - Optional fan-out child identity for per-host resolution
54
92
  * @returns Flat record of decrypted secret key-value pairs
55
93
  */
56
- function resolveSecretsForEnvironment(bindings, allSecrets, decryptFn) {
57
- const candidates = [];
58
- for (const binding of bindings) for (const secret of allSecrets) if (matchScopePattern(secret.scope, binding.scopePattern)) {
59
- const scopePath = stripScopePrefix(secret.scope);
60
- candidates.push({
61
- secret,
62
- scopeDepth: scopePath.split("/").length
63
- });
64
- }
94
+ function resolveSecretsForEnvironment(bindings, allSecrets, decryptFn, hostFacts) {
65
95
  const resolved = /* @__PURE__ */ new Map();
66
- for (const candidate of candidates) {
67
- const existing = resolved.get(candidate.secret.key);
68
- if (!existing || candidate.scopeDepth > existing.scopeDepth) resolved.set(candidate.secret.key, candidate);
96
+ for (const binding of bindings) {
97
+ const scopePattern = bindingScopeForHost(binding, hostFacts);
98
+ if (scopePattern === null) continue;
99
+ const hostSpec = hostSpecificity(binding.hostPattern);
100
+ for (const secret of allSecrets) {
101
+ if (!matchScopePattern(secret.scope, scopePattern)) continue;
102
+ const candidate = {
103
+ secret,
104
+ scopeDepth: stripScopePrefix(secret.scope).split("/").length,
105
+ hostSpec
106
+ };
107
+ if (candidateWins(candidate, resolved.get(secret.key))) resolved.set(secret.key, candidate);
108
+ }
69
109
  }
70
110
  const result = {};
71
111
  for (const [key, { secret }] of resolved) result[key] = decryptFn(secret);
@@ -0,0 +1,18 @@
1
+ import type { HostFacts } from './host-match.js';
2
+ /**
3
+ * Substitute `${agentId}` / `${host}` / `${label:NAME}` placeholders in a
4
+ * binding's `scope_pattern` with a fan-out child's identity, so one templated
5
+ * binding selects each host's own subtree (`prod/hosts/${agentId}/**`).
6
+ *
7
+ * Returns the substituted pattern, or `null` when the binding must be **skipped
8
+ * for this host** because either:
9
+ * - a referenced variable is unresolved (a missing `${label:NAME}`), or
10
+ * - a substituted value is unsafe — it contains anything outside
11
+ * `[A-Za-z0-9._-]` (a `/` path separator or a glob metacharacter), which
12
+ * could let a hostname/label value escape its segment or inject a pattern.
13
+ *
14
+ * A multi-valued label resolves to its lexicographic-first value
15
+ * (deterministic). A pattern with no placeholders is returned unchanged.
16
+ */
17
+ export declare function substituteScopePattern(pattern: string, facts: HostFacts): string | null;
18
+ //# sourceMappingURL=scope-template.d.ts.map
@@ -0,0 +1,43 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ //#region src/environment/scope-template.ts
3
+ /** A substituted value must be a single literal path segment — no `/` or glob. */
4
+ const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
5
+ const PLACEHOLDER = /\$\{(agentId|host|label:[^}]+)\}/g;
6
+ /**
7
+ * Substitute `${agentId}` / `${host}` / `${label:NAME}` placeholders in a
8
+ * binding's `scope_pattern` with a fan-out child's identity, so one templated
9
+ * binding selects each host's own subtree (`prod/hosts/${agentId}/**`).
10
+ *
11
+ * Returns the substituted pattern, or `null` when the binding must be **skipped
12
+ * for this host** because either:
13
+ * - a referenced variable is unresolved (a missing `${label:NAME}`), or
14
+ * - a substituted value is unsafe — it contains anything outside
15
+ * `[A-Za-z0-9._-]` (a `/` path separator or a glob metacharacter), which
16
+ * could let a hostname/label value escape its segment or inject a pattern.
17
+ *
18
+ * A multi-valued label resolves to its lexicographic-first value
19
+ * (deterministic). A pattern with no placeholders is returned unchanged.
20
+ */
21
+ function substituteScopePattern(pattern, facts) {
22
+ if (!pattern.includes("${")) return pattern;
23
+ let unresolved = false;
24
+ const out = pattern.replace(PLACEHOLDER, (_m, token) => {
25
+ let value;
26
+ if (token === "agentId") value = facts.agentId;
27
+ else if (token === "host") value = facts.host;
28
+ else {
29
+ const prefix = `${token.slice(6)}:`;
30
+ value = facts.labels.filter((l) => l.startsWith(prefix)).map((l) => l.slice(prefix.length)).sort()[0];
31
+ }
32
+ if (value === void 0 || !SAFE_SEGMENT.test(value)) {
33
+ unresolved = true;
34
+ return "";
35
+ }
36
+ return value;
37
+ });
38
+ return unresolved ? null : out;
39
+ }
40
+ //#endregion
41
+ export { substituteScopePattern };
42
+
43
+ //# sourceMappingURL=scope-template.js.map
@@ -4,6 +4,7 @@
4
4
  * Environments are org-level entities that group secrets, variables,
5
5
  * and protection rules for deployment targets (dev, staging, production).
6
6
  */
7
+ import { z } from 'zod';
7
8
  import type { ApproverClause } from '../approval/types.js';
8
9
  /** Environment entity — org-level deployment target with protection rules. */
9
10
  export interface Environment {
@@ -48,6 +49,13 @@ export interface EnvironmentBinding {
48
49
  orgId: string;
49
50
  environmentId: string;
50
51
  scopePattern: string;
52
+ /**
53
+ * Host selector this binding applies to (exact / glob / regex, matched
54
+ * against a fan-out child's agentId / hostname / labels). `'**'` matches
55
+ * every host, preserving fleet-wide behaviour for bindings with no host
56
+ * scope.
57
+ */
58
+ hostPattern: string;
51
59
  createdAt: string;
52
60
  }
53
61
  /** Non-secret key-value config per environment, with optional lock. */
@@ -72,8 +80,13 @@ export interface EnvironmentSourceOverride {
72
80
  createdAt: string;
73
81
  updatedAt: string;
74
82
  }
75
- /** Trust tier for contributor-based CI execution gating. */
76
- export type TrustTier = 'trusted' | 'known' | 'unknown';
83
+ /** Trust tier for contributor-based CI execution gating (single source of truth). */
84
+ export declare const TrustTierSchema: z.ZodEnum<{
85
+ unknown: "unknown";
86
+ known: "known";
87
+ trusted: "trusted";
88
+ }>;
89
+ export type TrustTier = z.infer<typeof TrustTierSchema>;
77
90
  /** Held run record for protection gate enforcement. */
78
91
  export interface HeldRun {
79
92
  id: string;
@@ -1,2 +1,19 @@
1
1
  import "../chunk-BTugEXQM.js";
2
- export {};
2
+ import { z } from "zod";
3
+ //#region src/environment/types.ts
4
+ /**
5
+ * Environment types for KiCI's deployment environment model.
6
+ *
7
+ * Environments are org-level entities that group secrets, variables,
8
+ * and protection rules for deployment targets (dev, staging, production).
9
+ */
10
+ /** Trust tier for contributor-based CI execution gating (single source of truth). */
11
+ const TrustTierSchema = z.enum([
12
+ "trusted",
13
+ "known",
14
+ "unknown"
15
+ ]);
16
+ //#endregion
17
+ export { TrustTierSchema };
18
+
19
+ //# sourceMappingURL=types.js.map