@lannguyensi/harness 0.47.0 → 0.49.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.
@@ -0,0 +1,241 @@
1
+ // Kubectl explicit-target parser, Risk Gate resolver merge (task
2
+ // a7eb1a71).
3
+ //
4
+ // Extracts an explicit `--context`/`--namespace`/`-n` target from a
5
+ // `kubectl ...` command string so `src/cli/policy/intercept.ts` can
6
+ // merge it into the resolver's `SignalInputs`, UPGRADE-ONLY (see that
7
+ // call site's `applyKubeTargetUpgrade` for the merge rule). See
8
+ // CHANGELOG.md's `[Unreleased]` entry for the measured defects this
9
+ // fixes and the review round history.
10
+ //
11
+ // SCOPE, deliberately narrow (task risk note: too-broad parsing could
12
+ // collect values from a foreign context):
13
+ //
14
+ // 1. Tokenization is anchored at the COMMAND HEAD: this module only
15
+ // recognizes a string whose very first token (after leading
16
+ // whitespace) is the literal word `kubectl`. It does not look
17
+ // through a leading `cd <path> &&`, `VAR=value`, or `git switch
18
+ // <branch> &&` prefix on its own; the CALLER is responsible for
19
+ // stripping any such prefix first (see `intercept.ts`, which
20
+ // passes the remainder after `parseBashPrefix` consumed one) so a
21
+ // wrapped invocation like `cd /tmp && kubectl ... --context x` or
22
+ // `KUBECONFIG=/tmp/k kubectl ... --context x` is still covered.
23
+ // Known unhandled shapes, listed so a future change does not have
24
+ // to rediscover them: a `sudo`/`time`/`env` wrapper immediately
25
+ // before `kubectl` (`parseBashPrefix` does not recognize those
26
+ // prefixes either); a kubectl invocation that is not the FIRST
27
+ // segment of a chained command (`echo hi && kubectl ...`); a piped
28
+ // kubectl (`foo | kubectl ...`, where kubectl is not the command
29
+ // head at all); an unquoted shell variable as a flag value
30
+ // (`--context $PROD` reads the literal text `$PROD`, never the
31
+ // variable's expanded value, so it practically never matches a
32
+ // real `kube_context_patterns` entry, the same "not evaluated"
33
+ // stance `bash-prefix-parse.ts` takes for `$VAR`/`${VAR}`); and
34
+ // backslash-escaped whitespace inside an unquoted value (`prod\
35
+ // eu` splits into two tokens at the escaped space, same as this
36
+ // runtime's other narrow parsers, none of which model shell
37
+ // escape sequences).
38
+ // 2. Once the head is confirmed to be `kubectl`, flags are read only
39
+ // from the FIRST shell segment of the command, up to (not
40
+ // including) the first unquoted `&&`, `||`, `;`, `|`, `&`, or
41
+ // newline, AND stopping at a bare `--` token (the POSIX
42
+ // end-of-flags marker).
43
+ // 3. Only `--context`, `--namespace`, and `-n` are recognized. The two
44
+ // long flags accept `--flag value` and `--flag=value`. `-n`
45
+ // additionally accepts the concatenated short-flag form `-nVALUE`
46
+ // (see the narrowing note at its own call site) and `-n=value`.
47
+ // Any other flag is ignored.
48
+ // 4. When a flag repeats, the LAST occurrence wins.
49
+ // 5. A flag with an EMPTY or whitespace-only value (`--context=`,
50
+ // `--context ""`) is treated as though the flag were absent, never
51
+ // as the literal empty string (an empty `SignalInputs.kubeContext`
52
+ // means "no context" to the resolver, so returning `""` here would
53
+ // erase a real ambient signal instead of leaving it alone).
54
+ //
55
+ // Never throws; a command that is not a `kubectl` invocation (by the
56
+ // narrow head test above) returns `{ context: null, namespace: null }`.
57
+ const EMPTY = { context: null, namespace: null };
58
+ const WS = /\s/;
59
+ function skipWs(s, i) {
60
+ while (i < s.length && WS.test(s[i]))
61
+ i++;
62
+ return i;
63
+ }
64
+ /** Empty or whitespace-only, per scope point 5 above. */
65
+ function isBlank(value) {
66
+ return value.trim().length === 0;
67
+ }
68
+ /**
69
+ * True when `command`'s head token (after skipping leading whitespace)
70
+ * is the literal word `kubectl` on a word boundary, immediately
71
+ * followed by whitespace or end-of-string, never a partial-word match
72
+ * (`kubectl-plugin` must not match). See module doc, scope point 1.
73
+ */
74
+ function isKubectlHead(command) {
75
+ const i = skipWs(command, 0);
76
+ if (command.slice(i, i + 7) !== "kubectl")
77
+ return false;
78
+ const after = i + 7;
79
+ return after >= command.length || WS.test(command[after]);
80
+ }
81
+ /**
82
+ * Slice `command` down to its first shell segment: everything up to
83
+ * (not including) the first unquoted `&&`, `||`, `;`, bare `|`, bare
84
+ * `&`, or newline. Quoted spans (single or double) are tracked so a
85
+ * chain-looking character inside a flag value's quotes does not end
86
+ * the segment early. See module doc, scope point 2.
87
+ */
88
+ function firstSegment(command) {
89
+ let inSingle = false;
90
+ let inDouble = false;
91
+ for (let i = 0; i < command.length; i++) {
92
+ const c = command[i];
93
+ if (inSingle) {
94
+ if (c === "'")
95
+ inSingle = false;
96
+ continue;
97
+ }
98
+ if (inDouble) {
99
+ if (c === '"')
100
+ inDouble = false;
101
+ continue;
102
+ }
103
+ if (c === "'") {
104
+ inSingle = true;
105
+ continue;
106
+ }
107
+ if (c === '"') {
108
+ inDouble = true;
109
+ continue;
110
+ }
111
+ if (c === "\n" || c === ";" || c === "|" || c === "&") {
112
+ return command.slice(0, i);
113
+ }
114
+ }
115
+ return command;
116
+ }
117
+ /**
118
+ * Whitespace-delimited tokenizer over an already-segment-bounded
119
+ * string. A quoted span (single or double) is read literally, quote
120
+ * characters stripped, no escape/interpolation handling, and may
121
+ * contain whitespace or a chain-boundary character (`;`, `|`) without
122
+ * splitting the token, mirroring the quoted-value handling
123
+ * `consumeInlineEnv` and `consumeLeadingCd` already use elsewhere in
124
+ * this runtime.
125
+ */
126
+ function tokenize(segment) {
127
+ const tokens = [];
128
+ let i = 0;
129
+ const n = segment.length;
130
+ while (i < n) {
131
+ i = skipWs(segment, i);
132
+ if (i >= n)
133
+ break;
134
+ let token = "";
135
+ while (i < n && !WS.test(segment[i])) {
136
+ const c = segment[i];
137
+ if (c === "'" || c === '"') {
138
+ const end = segment.indexOf(c, i + 1);
139
+ if (end < 0) {
140
+ token += segment.slice(i + 1);
141
+ i = n;
142
+ break;
143
+ }
144
+ token += segment.slice(i + 1, end);
145
+ i = end + 1;
146
+ continue;
147
+ }
148
+ token += c;
149
+ i++;
150
+ }
151
+ tokens.push(token);
152
+ }
153
+ return tokens;
154
+ }
155
+ /**
156
+ * Parse an explicit `--context`/`--namespace`/`-n` target out of a
157
+ * `kubectl ...` command string. Returns `{ context: null, namespace:
158
+ * null }` when `command` is not, by the narrow head test above, a
159
+ * `kubectl` invocation, or when neither flag carries a non-blank value.
160
+ * Never throws.
161
+ */
162
+ export function parseKubectlTarget(command) {
163
+ if (typeof command !== "string" || command.length === 0)
164
+ return EMPTY;
165
+ if (!isKubectlHead(command))
166
+ return EMPTY;
167
+ const tokens = tokenize(firstSegment(command));
168
+ let context = null;
169
+ let namespace = null;
170
+ for (let i = 0; i < tokens.length; i++) {
171
+ const t = tokens[i];
172
+ // POSIX end-of-flags marker (scope point 2): everything after a
173
+ // bare `--` belongs to the exec'd program, not to kubectl itself.
174
+ if (t === "--")
175
+ break;
176
+ if (t === "--context" || t === "--namespace" || t === "-n") {
177
+ const value = tokens[i + 1];
178
+ // No next token: a trailing valueless flag, nothing to consume or
179
+ // skip. Otherwise the NEXT token is this flag's value under real
180
+ // kubectl argument parsing (`--context -n prod` means --context
181
+ // takes the literal value "-n"; "prod" is a positional argument,
182
+ // not `-n`'s own value), so advance `i` past it here, regardless
183
+ // of whether the value turns out blank, so the loop's own
184
+ // increment does not re-examine that same token as if it were an
185
+ // independent flag (fix round 3, review LOW finding).
186
+ if (value === undefined)
187
+ continue;
188
+ i++;
189
+ if (isBlank(value))
190
+ continue;
191
+ if (t === "--context")
192
+ context = value;
193
+ else
194
+ namespace = value;
195
+ continue;
196
+ }
197
+ if (t.startsWith("--context=")) {
198
+ const value = t.slice("--context=".length);
199
+ if (!isBlank(value))
200
+ context = value;
201
+ continue;
202
+ }
203
+ if (t.startsWith("--namespace=")) {
204
+ const value = t.slice("--namespace=".length);
205
+ if (!isBlank(value))
206
+ namespace = value;
207
+ continue;
208
+ }
209
+ // `-n` short-flag forms, checked in an order where the longer
210
+ // prefix (`-n=`) is tried before the general concatenated-value
211
+ // fallback, so `-n=value` does not fall through to being read as
212
+ // namespace `=value` (scope point 3).
213
+ if (t.startsWith("-n=")) {
214
+ const value = t.slice("-n=".length);
215
+ if (!isBlank(value))
216
+ namespace = value;
217
+ continue;
218
+ }
219
+ if (t.startsWith("-n") && t.length > 2) {
220
+ // Narrowed (fix round 3, review LOW finding): real pflag DOES
221
+ // glue any value onto a short flag's remaining characters, which
222
+ // makes an unrelated flag typed with a single dash instead of two
223
+ // (`-no-headers` for `--no-headers`) collide with this form and
224
+ // read as `-n` plus a nonsense namespace value `o-headers`. A
225
+ // genuine namespace value never contains a `-`-prefixed run once
226
+ // read this way, since a Kubernetes namespace is a plain DNS
227
+ // label; requiring no embedded `-` rejects that whole class of
228
+ // false positive. This does NOT catch every case (`-namespace`
229
+ // slices to `amespace`, which has no dash and is still accepted;
230
+ // this is genuine pflag behavior for a value-taking short flag
231
+ // and is left as a documented, narrow residual gap, not a
232
+ // fabricated denylist of flag-shaped words).
233
+ const value = t.slice(2);
234
+ if (!isBlank(value) && !value.includes("-"))
235
+ namespace = value;
236
+ continue;
237
+ }
238
+ }
239
+ return { context, namespace };
240
+ }
241
+ //# sourceMappingURL=kubectl-target-parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kubectl-target-parse.js","sourceRoot":"","sources":["../../src/runtime/kubectl-target-parse.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lannguyensi/harness",
3
- "version": "0.47.0",
4
- "description": "Declarative control plane for agent harnesses \u2014 one YAML for grounding, tools, memory, and hooks.",
3
+ "version": "0.49.0",
4
+ "description": "Declarative control plane for agent harnesses — one YAML for grounding, tools, memory, and hooks.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/LanNguyenSi/harness",
7
7
  "repository": {