@kici-dev/engine 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/access-log-policy.js +2 -0
- package/dist/audit/activity.d.ts +3 -1
- package/dist/audit/activity.js +8 -16
- package/dist/audit/retention-policy.js +4 -0
- package/dist/environment/host-match.d.ts +25 -0
- package/dist/environment/host-match.js +67 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/scope-resolver.d.ts +15 -4
- package/dist/environment/scope-resolver.js +55 -15
- package/dist/environment/scope-template.d.ts +18 -0
- package/dist/environment/scope-template.js +43 -0
- package/dist/environment/types.d.ts +7 -0
- package/dist/fanout/materialize.d.ts +34 -0
- package/dist/fanout/materialize.js +72 -45
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -5
- package/dist/inputs/build.d.ts +9 -0
- package/dist/inputs/build.js +44 -0
- package/dist/inputs/coerce.d.ts +25 -0
- package/dist/inputs/coerce.js +51 -0
- package/dist/inputs/descriptor.d.ts +53 -0
- package/dist/inputs/descriptor.js +42 -0
- package/dist/inputs/extract.d.ts +15 -0
- package/dist/inputs/extract.js +117 -0
- package/dist/inputs/index.d.ts +5 -0
- package/dist/inputs/index.js +6 -0
- package/dist/labels/compile.d.ts +9 -0
- package/dist/labels/compile.js +10 -1
- package/dist/labels.d.ts +29 -0
- package/dist/labels.js +32 -1
- package/dist/metrics/catalog-policy.d.ts +7 -0
- package/dist/metrics/catalog-policy.js +11 -12
- package/dist/metrics/metric-catalog.generated.d.ts +2 -2
- package/dist/metrics/metric-catalog.generated.js +10 -2
- package/dist/protocol/messages/access-log.d.ts +15 -0
- package/dist/protocol/messages/access-log.js +4 -1
- package/dist/protocol/messages/dashboard.d.ts +146 -6
- package/dist/protocol/messages/dashboard.js +50 -10
- package/dist/protocol/messages/platform-orchestrator.d.ts +33 -3
- package/dist/protocol/messages/run-events.d.ts +1 -1
- package/dist/trigger/types.d.ts +41 -1
- package/dist/trigger/types.js +14 -2
- package/package.json +9 -1
- package/sbom.spdx.json +5 -5
|
@@ -152,6 +152,8 @@ const POLICY_BY_ACTION = {
|
|
|
152
152
|
"backend.test": { kind: "always" },
|
|
153
153
|
"fleet.host.declare": { kind: "always" },
|
|
154
154
|
"fleet.host.remove": { kind: "always" },
|
|
155
|
+
"fleet.init_runner.bringup": { kind: "always" },
|
|
156
|
+
"fleet.pre_boot.send": { kind: "always" },
|
|
155
157
|
"global_workflows.update": { kind: "always" },
|
|
156
158
|
"org_settings.dashboard_write_policy.update": { kind: "always" },
|
|
157
159
|
"cluster_name.update": { kind: "always" },
|
package/dist/audit/activity.d.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -76,6 +76,7 @@ export declare const activityRowSchema: z.ZodObject<{
|
|
|
76
76
|
platform_proxy: "platform_proxy";
|
|
77
77
|
admin_http: "admin_http";
|
|
78
78
|
admin_cli: "admin_cli";
|
|
79
|
+
agent: "agent";
|
|
79
80
|
}>>>;
|
|
80
81
|
}, z.core.$strip>;
|
|
81
82
|
export type ActivityRow = z.infer<typeof activityRowSchema>;
|
|
@@ -108,6 +109,7 @@ export declare const activityFilterSchema: z.ZodObject<{
|
|
|
108
109
|
platform_proxy: "platform_proxy";
|
|
109
110
|
admin_http: "admin_http";
|
|
110
111
|
admin_cli: "admin_cli";
|
|
112
|
+
agent: "agent";
|
|
111
113
|
}>>;
|
|
112
114
|
targetType: z.ZodOptional<z.ZodString>;
|
|
113
115
|
targetId: z.ZodOptional<z.ZodString>;
|
package/dist/audit/activity.js
CHANGED
|
@@ -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 (
|
|
75
|
-
* admin_cli). Renamed from `source` on
|
|
76
|
-
* unified row's `source` field is
|
|
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:
|
|
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:
|
|
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. */
|
|
@@ -65,6 +65,8 @@ const ACCESS_LOG_WARM_DAYS = {
|
|
|
65
65
|
"backend.test": 180,
|
|
66
66
|
"fleet.host.declare": 180,
|
|
67
67
|
"fleet.host.remove": 180,
|
|
68
|
+
"fleet.init_runner.bringup": 180,
|
|
69
|
+
"fleet.pre_boot.send": 180,
|
|
68
70
|
"global_workflows.update": 180,
|
|
69
71
|
"org_settings.dashboard_write_policy.update": 180,
|
|
70
72
|
"cluster_name.update": 180,
|
|
@@ -307,6 +309,8 @@ const ACCESS_LOG_COLD_DAYS = {
|
|
|
307
309
|
"backend.test": 730,
|
|
308
310
|
"fleet.host.declare": 730,
|
|
309
311
|
"fleet.host.remove": 730,
|
|
312
|
+
"fleet.init_runner.bringup": 730,
|
|
313
|
+
"fleet.pre_boot.send": 730,
|
|
310
314
|
"global_workflows.update": 730,
|
|
311
315
|
"org_settings.dashboard_write_policy.update": 730,
|
|
312
316
|
"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,4 @@
|
|
|
1
1
|
export type { Environment, EnvironmentBinding, EnvironmentVariable, EnvironmentSourceOverride, ScopedSecret, HeldRun, ProtectionGateResult, TrustTier, } from './types.js';
|
|
2
2
|
export { resolveSecretsForEnvironment, matchScopePattern, stripScopePrefix, } from './scope-resolver.js';
|
|
3
|
+
export type { HostFacts } from './host-match.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.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
|
|
26
|
-
* When multiple scopes provide the same key,
|
|
27
|
-
*
|
|
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
|
|
48
|
-
* When multiple scopes provide the same key,
|
|
49
|
-
*
|
|
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
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
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
|
|
@@ -48,6 +48,13 @@ export interface EnvironmentBinding {
|
|
|
48
48
|
orgId: string;
|
|
49
49
|
environmentId: string;
|
|
50
50
|
scopePattern: string;
|
|
51
|
+
/**
|
|
52
|
+
* Host selector this binding applies to (exact / glob / regex, matched
|
|
53
|
+
* against a fan-out child's agentId / hostname / labels). `'**'` matches
|
|
54
|
+
* every host, preserving fleet-wide behaviour for bindings with no host
|
|
55
|
+
* scope.
|
|
56
|
+
*/
|
|
57
|
+
hostPattern: string;
|
|
51
58
|
createdAt: string;
|
|
52
59
|
}
|
|
53
60
|
/** Non-secret key-value config per environment, with optional lock. */
|
|
@@ -53,6 +53,13 @@ export interface ResolvedHostAgent {
|
|
|
53
53
|
arch?: string;
|
|
54
54
|
/** Which orchestrator owns the live WS (null = not currently connected). */
|
|
55
55
|
connectedInstanceId?: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* True when this host is a declared-but-un-agented `includeUninitialized`
|
|
58
|
+
* target: it has no live agent, so the dispatcher first brings up a temporary
|
|
59
|
+
* init-runner over SSH before the child's pinned steps can land. False / absent
|
|
60
|
+
* for a live host (runs on its own agent directly).
|
|
61
|
+
*/
|
|
62
|
+
needsBringup?: boolean;
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
65
|
* One dispatchable unit produced from a lock job. `variantKind` is `matrix` for
|
|
@@ -83,6 +90,20 @@ export interface MaterializedJob {
|
|
|
83
90
|
agent?: ResolvedHostAgent;
|
|
84
91
|
/** Which orchestrator owns the pinned agent's live WS (null = not connected). */
|
|
85
92
|
connectedInstanceId?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* True for a declared-but-un-agented `includeUninitialized` host child: the
|
|
95
|
+
* dispatcher brings up a temporary init-runner over SSH before its pinned
|
|
96
|
+
* steps land. Mirrors `agent.needsBringup`.
|
|
97
|
+
*/
|
|
98
|
+
needsBringup?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* 0-based position of this child in the deterministically-ordered fan-out
|
|
101
|
+
* (host: by `agentId`; matrix: by variant label). Exposed to the agent as
|
|
102
|
+
* `ctx.fanout.index`. Absent on a non-fan-out (single-child) job.
|
|
103
|
+
*/
|
|
104
|
+
fanoutIndex?: number;
|
|
105
|
+
/** Number of children in this fan-out. Exposed as `ctx.fanout.total`. */
|
|
106
|
+
fanoutTotal?: number;
|
|
86
107
|
}
|
|
87
108
|
export interface FanoutResult {
|
|
88
109
|
jobs: MaterializedJob[];
|
|
@@ -101,6 +122,17 @@ export declare function matrixEnvelopeFields(mat: MaterializedJob): {
|
|
|
101
122
|
name: string;
|
|
102
123
|
baseJobName: string;
|
|
103
124
|
matrixValues?: MatrixValues;
|
|
125
|
+
fanoutIndex?: number;
|
|
126
|
+
fanoutTotal?: number;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The fan-out position envelope fields (`fanoutIndex`/`fanoutTotal`) of a
|
|
130
|
+
* materialized child, shared by the matrix + host envelope builders. Both are
|
|
131
|
+
* present together or absent together (a non-fan-out child has neither).
|
|
132
|
+
*/
|
|
133
|
+
export declare function fanoutEnvelopeFields(mat: MaterializedJob): {
|
|
134
|
+
fanoutIndex?: number;
|
|
135
|
+
fanoutTotal?: number;
|
|
104
136
|
};
|
|
105
137
|
/**
|
|
106
138
|
* Build materialized children from a resolved dynamic-matrix combination list
|
|
@@ -122,6 +154,8 @@ export declare function hostEnvelopeFields(mat: MaterializedJob): {
|
|
|
122
154
|
host?: string;
|
|
123
155
|
agent?: ResolvedHostAgent;
|
|
124
156
|
connectedInstanceId?: string | null;
|
|
157
|
+
fanoutIndex?: number;
|
|
158
|
+
fanoutTotal?: number;
|
|
125
159
|
};
|
|
126
160
|
/**
|
|
127
161
|
* Build materialized host children from a resolved target-host list (produced by
|
|
@@ -48,6 +48,30 @@ var FanoutError = class FanoutError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
|
+
* Stamp a deterministic `fanoutIndex`/`fanoutTotal` onto each child of a
|
|
52
|
+
* multi-child fan-out. `fanoutIndex` is the child's rank in the order defined by
|
|
53
|
+
* `keyOf` (host: `agentId`; matrix: variant label), independent of emission
|
|
54
|
+
* order — the orchestrator's wave dispatch keys on `fanoutIndex`, so emission
|
|
55
|
+
* order need not be touched (this preserves the local-executor matrix naming
|
|
56
|
+
* order). A single child gets no position (non-fan-out job; `ctx.fanout` stays
|
|
57
|
+
* undefined). Mutates and returns the same array.
|
|
58
|
+
*/
|
|
59
|
+
function assignFanoutPositions(children, keyOf) {
|
|
60
|
+
if (children.length <= 1) return children;
|
|
61
|
+
const total = children.length;
|
|
62
|
+
const rank = /* @__PURE__ */ new Map();
|
|
63
|
+
[...children].sort((a, b) => keyOf(a).localeCompare(keyOf(b))).forEach((child, index) => rank.set(child, index));
|
|
64
|
+
for (const child of children) {
|
|
65
|
+
child.fanoutIndex = rank.get(child);
|
|
66
|
+
child.fanoutTotal = total;
|
|
67
|
+
}
|
|
68
|
+
return children;
|
|
69
|
+
}
|
|
70
|
+
/** Variant label of a child = the text inside the trailing `(...)` of its expanded name. */
|
|
71
|
+
function variantLabelOf(child) {
|
|
72
|
+
return child.expandedName.slice(child.baseName.length + 2, -1);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
51
75
|
* The job-config envelope fields that identify a materialized child: the
|
|
52
76
|
* expanded `name`, the `baseJobName` (what the agent exposes as `ctx.job.name`),
|
|
53
77
|
* and `matrixValues` (exposed as `ctx.matrix`). Every dispatch site spreads
|
|
@@ -59,10 +83,22 @@ function matrixEnvelopeFields(mat) {
|
|
|
59
83
|
return {
|
|
60
84
|
name: mat.expandedName,
|
|
61
85
|
baseJobName: mat.baseName,
|
|
62
|
-
...mat.variantValues && { matrixValues: mat.variantValues }
|
|
86
|
+
...mat.variantValues && { matrixValues: mat.variantValues },
|
|
87
|
+
...fanoutEnvelopeFields(mat)
|
|
63
88
|
};
|
|
64
89
|
}
|
|
65
90
|
/**
|
|
91
|
+
* The fan-out position envelope fields (`fanoutIndex`/`fanoutTotal`) of a
|
|
92
|
+
* materialized child, shared by the matrix + host envelope builders. Both are
|
|
93
|
+
* present together or absent together (a non-fan-out child has neither).
|
|
94
|
+
*/
|
|
95
|
+
function fanoutEnvelopeFields(mat) {
|
|
96
|
+
return mat.fanoutTotal !== void 0 ? {
|
|
97
|
+
fanoutIndex: mat.fanoutIndex,
|
|
98
|
+
fanoutTotal: mat.fanoutTotal
|
|
99
|
+
} : {};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
66
102
|
* Build materialized children from a resolved dynamic-matrix combination list
|
|
67
103
|
* (produced by the agent eval flow). Mirrors the static-matrix branch of
|
|
68
104
|
* {@link materializeFanout} but takes the already-resolved combinations and
|
|
@@ -72,21 +108,17 @@ function materializeResolvedMatrix(lockJob, combos) {
|
|
|
72
108
|
if (combos.length === 0) throw new FanoutError(lockJob.name, `dynamic matrix for job '${lockJob.name}' resolved to zero combinations`);
|
|
73
109
|
if (combos.length > 256) throw new FanoutError(lockJob.name, `dynamic matrix for job '${lockJob.name}' resolved to ${combos.length} combinations (max 256)`);
|
|
74
110
|
const jobs = [];
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
variantKind: "matrix",
|
|
84
|
-
variantValues
|
|
85
|
-
});
|
|
86
|
-
}
|
|
111
|
+
for (const variantValues of combos) jobs.push({
|
|
112
|
+
lockJob,
|
|
113
|
+
baseName: lockJob.name,
|
|
114
|
+
expandedName: formatExpandedJobName(lockJob.name, variantValues),
|
|
115
|
+
variantKind: "matrix",
|
|
116
|
+
variantValues
|
|
117
|
+
});
|
|
118
|
+
assignFanoutPositions(jobs, variantLabelOf);
|
|
87
119
|
return {
|
|
88
120
|
jobs,
|
|
89
|
-
expansionMap: new Map([[lockJob.name,
|
|
121
|
+
expansionMap: new Map([[lockJob.name, jobs.map((j) => j.expandedName)]])
|
|
90
122
|
};
|
|
91
123
|
}
|
|
92
124
|
/**
|
|
@@ -102,7 +134,8 @@ function hostEnvelopeFields(mat) {
|
|
|
102
134
|
...mat.pinnedAgentId && { pinnedAgentId: mat.pinnedAgentId },
|
|
103
135
|
...mat.host && { host: mat.host },
|
|
104
136
|
...mat.agent && { agent: mat.agent },
|
|
105
|
-
...mat.connectedInstanceId !== void 0 && { connectedInstanceId: mat.connectedInstanceId }
|
|
137
|
+
...mat.connectedInstanceId !== void 0 && { connectedInstanceId: mat.connectedInstanceId },
|
|
138
|
+
...fanoutEnvelopeFields(mat)
|
|
106
139
|
};
|
|
107
140
|
}
|
|
108
141
|
/**
|
|
@@ -116,24 +149,21 @@ function materializeResolvedHosts(lockJob, agents, maxHosts) {
|
|
|
116
149
|
if (agents.length === 0) throw new FanoutError(lockJob.name, `runsOnAll for job '${lockJob.name}' matched zero matching hosts`);
|
|
117
150
|
if (agents.length > maxHosts) throw new FanoutError(lockJob.name, `runsOnAll for job '${lockJob.name}' matched ${agents.length} hosts (max ${maxHosts})`);
|
|
118
151
|
const jobs = [];
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
connectedInstanceId: agent.connectedInstanceId ?? null
|
|
132
|
-
});
|
|
133
|
-
}
|
|
152
|
+
for (const agent of agents) jobs.push({
|
|
153
|
+
lockJob,
|
|
154
|
+
baseName: lockJob.name,
|
|
155
|
+
expandedName: `${lockJob.name} (${agent.host})`,
|
|
156
|
+
variantKind: "host",
|
|
157
|
+
pinnedAgentId: agent.agentId,
|
|
158
|
+
host: agent.host,
|
|
159
|
+
agent,
|
|
160
|
+
connectedInstanceId: agent.connectedInstanceId ?? null,
|
|
161
|
+
...agent.needsBringup && { needsBringup: true }
|
|
162
|
+
});
|
|
163
|
+
assignFanoutPositions(jobs, (child) => child.pinnedAgentId ?? "");
|
|
134
164
|
return {
|
|
135
165
|
jobs,
|
|
136
|
-
expansionMap: new Map([[lockJob.name,
|
|
166
|
+
expansionMap: new Map([[lockJob.name, jobs.map((j) => j.expandedName)]])
|
|
137
167
|
};
|
|
138
168
|
}
|
|
139
169
|
/**
|
|
@@ -170,19 +200,16 @@ function materializeFanout(staticJobs) {
|
|
|
170
200
|
const combos = applyIncludeExclude(expandMatrix(values), lockJob.include, lockJob.exclude);
|
|
171
201
|
if (combos.length === 0) throw new FanoutError(lockJob.name, `matrix for job '${lockJob.name}' expands to zero combinations`);
|
|
172
202
|
if (combos.length > 256) throw new FanoutError(lockJob.name, `matrix for job '${lockJob.name}' expands to ${combos.length} combinations (max 256)`);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
expansionMap.set(lockJob.name, names);
|
|
203
|
+
const children = combos.map((variantValues) => ({
|
|
204
|
+
lockJob,
|
|
205
|
+
baseName: lockJob.name,
|
|
206
|
+
expandedName: formatExpandedJobName(lockJob.name, variantValues),
|
|
207
|
+
variantKind: "matrix",
|
|
208
|
+
variantValues
|
|
209
|
+
}));
|
|
210
|
+
assignFanoutPositions(children, variantLabelOf);
|
|
211
|
+
jobs.push(...children);
|
|
212
|
+
expansionMap.set(lockJob.name, children.map((j) => j.expandedName));
|
|
186
213
|
}
|
|
187
214
|
return {
|
|
188
215
|
jobs,
|
|
@@ -190,6 +217,6 @@ function materializeFanout(staticJobs) {
|
|
|
190
217
|
};
|
|
191
218
|
}
|
|
192
219
|
//#endregion
|
|
193
|
-
export { FanoutCause, FanoutError, MAX_FANOUT_JOBS, VariantKind, hostEnvelopeFields, materializeFanout, materializeResolvedHosts, materializeResolvedMatrix, matrixEnvelopeFields };
|
|
220
|
+
export { FanoutCause, FanoutError, MAX_FANOUT_JOBS, VariantKind, fanoutEnvelopeFields, hostEnvelopeFields, materializeFanout, materializeResolvedHosts, materializeResolvedMatrix, matrixEnvelopeFields };
|
|
194
221
|
|
|
195
222
|
//# sourceMappingURL=materialize.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from './trigger/types.js';
|
|
|
36
36
|
export * from './trigger/trigger-event-type.js';
|
|
37
37
|
export * from './trigger/decision-trace.js';
|
|
38
38
|
export * from './trigger/matcher.js';
|
|
39
|
+
export * from './inputs/index.js';
|
|
39
40
|
export * from './state-machine/index.js';
|
|
40
41
|
export * from './provider/index.js';
|
|
41
42
|
export { githubWebhookPath } from './webhook/webhook-url-format.js';
|
|
@@ -46,7 +47,7 @@ export type { RateLimiterConfig, RateLimitResult } from './ws/rate-limiter.js';
|
|
|
46
47
|
export * from './env/environment-allowlist.js';
|
|
47
48
|
export * from './secrets/index.js';
|
|
48
49
|
export * from './environment/index.js';
|
|
49
|
-
export { deriveOsArchLabels, hostLabel, parseHostLabel, HOST_LABEL_PREFIX, agentTypeLabel, scalerLabel, mergeAutoLabels, normalizeRunsOn, KNOWN_ROLES, resolveRoleLabels, validateNoReservedLabels, scalerAgentLabels, isSelfReportedLabel, SELF_REPORTED_LABEL_PREFIXES, } from './labels.js';
|
|
50
|
+
export { deriveOsArchLabels, hostLabel, parseHostLabel, HOST_LABEL_PREFIX, agentTypeLabel, scalerLabel, mergeAutoLabels, normalizeRunsOn, KNOWN_ROLES, resolveRoleLabels, validateNoReservedLabels, scalerAgentLabels, isSelfReportedLabel, SELF_REPORTED_LABEL_PREFIXES, CAPABILITY_LABEL_PREFIX, capabilityLabel, SSH_TRANSPORT_CAPABILITY, INIT_LABEL, PRIVILEGED_ROOT_LABEL, } from './labels.js';
|
|
50
51
|
export type { NormalizedRunsOn } from './labels.js';
|
|
51
52
|
export type { AgentRole } from './labels.js';
|
|
52
53
|
export { LabelMatcher, matcherMatches, matcherSatisfiedBy, partitionMatchers, compileRegexMatcher, HostTargetValue, HostTargetSelector, hostSatisfiesTarget, } from './labels-match.js';
|