@knowttl/kubernetes-axi 0.0.0-stage → 0.1.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.
- package/README.md +306 -2
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/axi-credential.schema.json +31 -0
- package/bin/kubernetes-axi.js +10 -0
- package/command-metadata.json +36947 -0
- package/dist/byte-process.d.ts +9 -0
- package/dist/byte-process.js +56 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +68 -0
- package/dist/credentials.d.ts +28 -0
- package/dist/credentials.js +113 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +11 -0
- package/dist/exec-shim.d.ts +1 -0
- package/dist/exec-shim.js +16 -0
- package/dist/grammar.d.ts +50 -0
- package/dist/grammar.js +248 -0
- package/dist/hosted-session.d.ts +1 -0
- package/dist/hosted-session.js +201 -0
- package/dist/metadata-pin.d.ts +3 -0
- package/dist/metadata-pin.js +4 -0
- package/dist/mutations.d.ts +11 -0
- package/dist/mutations.js +526 -0
- package/dist/native.d.ts +26 -0
- package/dist/native.js +143 -0
- package/dist/private-files.d.ts +9 -0
- package/dist/private-files.js +137 -0
- package/dist/protobuf-schema.d.ts +2 -0
- package/dist/protobuf-schema.js +1 -0
- package/dist/protobuf.d.ts +1 -0
- package/dist/protobuf.js +67 -0
- package/dist/redaction.d.ts +10 -0
- package/dist/redaction.js +50 -0
- package/dist/remote-session.d.ts +22 -0
- package/dist/remote-session.js +90 -0
- package/dist/route-policy.d.ts +1 -0
- package/dist/route-policy.js +19 -0
- package/dist/runner.d.ts +11 -0
- package/dist/runner.js +367 -0
- package/dist/workspace-tar.d.ts +6 -0
- package/dist/workspace-tar.js +109 -0
- package/docs/hooks.md +35 -0
- package/kubectl-pins.json +18 -0
- package/package.json +21 -4
- package/skills/kubernetes-axi/SKILL.md +32 -0
- package/third-party/kubectl-LICENSE.txt +202 -0
- package/vendor/kubectl-linux-amd64 +4 -0
- package/vendor/kubectl-windows-amd64.exe +4 -0
package/dist/runner.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile, rm, realpath } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { Ajv } from 'ajv';
|
|
9
|
+
import { Failure } from './errors.js';
|
|
10
|
+
import { privateDirectory, writePrivate } from './private-files.js';
|
|
11
|
+
import { readPrivate } from './private-files.js';
|
|
12
|
+
import { nativeConfig, inspectNative } from './native.js';
|
|
13
|
+
import { mutationSession } from './mutations.js';
|
|
14
|
+
import { byteProcess } from './byte-process.js';
|
|
15
|
+
import { clientOnly } from './grammar.js';
|
|
16
|
+
const execute = promisify(execFile);
|
|
17
|
+
const pins = JSON.parse(await readFile(new URL('../kubectl-pins.json', import.meta.url), 'utf8'));
|
|
18
|
+
const ajv = new Ajv();
|
|
19
|
+
const validVersion = ajv.compile({
|
|
20
|
+
type: 'object', required: ['major', 'minor', 'gitVersion'],
|
|
21
|
+
properties: { major: { const: '1' }, minor: { type: 'string', pattern: '^[0-9]+\\+?$' }, gitVersion: { type: 'string' } },
|
|
22
|
+
});
|
|
23
|
+
function compatibleVersion(value) {
|
|
24
|
+
if (!validVersion(value))
|
|
25
|
+
throw new Failure('UPSTREAM_INVALID', 'The server version response is invalid.');
|
|
26
|
+
if (!pins.supportedServerMinors.includes(Number.parseInt(value.minor, 10)))
|
|
27
|
+
throw new Failure('BINARY_INCOMPATIBLE', 'Server version is outside the recorded kubectl skew matrix.');
|
|
28
|
+
}
|
|
29
|
+
const validObject = ajv.compile({
|
|
30
|
+
type: 'object', required: ['metadata'], properties: {
|
|
31
|
+
apiVersion: { type: 'string' }, kind: { type: 'string' },
|
|
32
|
+
metadata: { type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 1 }, namespace: { type: 'string' } } },
|
|
33
|
+
status: { type: 'object', properties: {
|
|
34
|
+
phase: { type: 'string' }, availableReplicas: { type: 'integer' },
|
|
35
|
+
conditions: { type: 'array', items: { type: 'object', required: ['type', 'status'], properties: { type: { type: 'string' }, status: { type: 'string' } } } },
|
|
36
|
+
} },
|
|
37
|
+
spec: { type: 'object', properties: { replicas: { type: 'integer' } } },
|
|
38
|
+
usage: { type: 'object', required: ['cpu', 'memory'], properties: { cpu: { type: 'string' }, memory: { type: 'string' } } },
|
|
39
|
+
containers: { type: 'array', items: { type: 'object', required: ['name', 'usage'], properties: {
|
|
40
|
+
name: { type: 'string' }, usage: { type: 'object', required: ['cpu', 'memory'], properties: { cpu: { type: 'string' }, memory: { type: 'string' } } },
|
|
41
|
+
} } },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const validPage = ajv.compile({
|
|
45
|
+
type: 'object', required: ['apiVersion', 'kind', 'metadata', 'items'], properties: {
|
|
46
|
+
apiVersion: { type: 'string' }, kind: { type: 'string' }, items: { type: 'array' },
|
|
47
|
+
metadata: { type: 'object', properties: { continue: { type: 'string' }, resourceVersion: { type: 'string' }, remainingItemCount: { type: 'integer', minimum: 0 } } },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const validDiscovery = ajv.compile({
|
|
51
|
+
type: 'object', required: ['groupVersion', 'resources'], properties: {
|
|
52
|
+
groupVersion: { type: 'string' }, resources: { type: 'array', items: {
|
|
53
|
+
type: 'object', required: ['name', 'singularName', 'namespaced', 'kind', 'verbs'], properties: {
|
|
54
|
+
name: { type: 'string', pattern: '^[a-z0-9][a-z0-9./-]*$' }, singularName: { type: 'string' }, kind: { type: 'string', minLength: 1 }, namespaced: { type: 'boolean' },
|
|
55
|
+
verbs: { type: 'array', items: { type: 'string' } }, shortNames: { type: 'array', items: { type: 'string' } },
|
|
56
|
+
},
|
|
57
|
+
} },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const validCore = ajv.compile({
|
|
61
|
+
type: 'object', required: ['versions'], properties: { versions: { type: 'array', minItems: 1, items: { type: 'string', pattern: '^v[0-9]+((alpha|beta)[0-9]+)?$' } } },
|
|
62
|
+
});
|
|
63
|
+
const groupVersionSchema = { type: 'object', required: ['groupVersion'], properties: { groupVersion: { type: 'string', pattern: '^[a-z0-9][a-z0-9.-]*/v[0-9]+((alpha|beta)[0-9]+)?$' } } };
|
|
64
|
+
const validGroups = ajv.compile({
|
|
65
|
+
type: 'object', required: ['groups'], properties: { groups: { type: 'array', items: {
|
|
66
|
+
type: 'object', required: ['name', 'versions', 'preferredVersion'], properties: {
|
|
67
|
+
name: { type: 'string', pattern: '^[a-z0-9][a-z0-9.-]*$' },
|
|
68
|
+
versions: { type: 'array', minItems: 1, items: groupVersionSchema },
|
|
69
|
+
preferredVersion: groupVersionSchema,
|
|
70
|
+
},
|
|
71
|
+
} } },
|
|
72
|
+
});
|
|
73
|
+
export async function readCluster(identity, invocation, redactor, byteExecution) {
|
|
74
|
+
const pin = pins.binaries[`${process.platform}-${process.arch}`];
|
|
75
|
+
if (!pin)
|
|
76
|
+
throw new Failure('BINARY_UNSUPPORTED', 'Use a recorded Linux or Windows amd64 runtime.');
|
|
77
|
+
const binary = await realpath(fileURLToPath(new URL(`../vendor/${pin.file}`, import.meta.url)));
|
|
78
|
+
const digest = createHash('sha256').update(await readFile(binary)).digest('hex');
|
|
79
|
+
if (digest !== pin.sha256)
|
|
80
|
+
throw new Failure('BINARY_INTEGRITY', 'Restore the recorded managed kubectl binary; PATH fallback is disabled.');
|
|
81
|
+
const flags = invocation.flags;
|
|
82
|
+
let namespace = flags.get('namespace') ?? identity?.profile.namespace ?? 'default';
|
|
83
|
+
const all = flags.get('all-namespaces') === 'true';
|
|
84
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) || namespace.length > 63)
|
|
85
|
+
throw new Failure('USAGE', 'Use a valid Kubernetes namespace.', 2);
|
|
86
|
+
const directory = join(tmpdir(), `kubernetes-axi-${randomUUID()}`);
|
|
87
|
+
await privateDirectory(directory);
|
|
88
|
+
const profile = identity?.profile;
|
|
89
|
+
const secret = identity?.secret;
|
|
90
|
+
const user = profile?.auth === 'token' ? { token: secret } : {
|
|
91
|
+
'client-certificate-data': Buffer.from(profile?.clientCertificate ?? '').toString('base64'), 'client-key-data': Buffer.from(secret ?? '').toString('base64'),
|
|
92
|
+
};
|
|
93
|
+
const config = join(directory, 'config.json');
|
|
94
|
+
const cancellation = new AbortController();
|
|
95
|
+
const cancel = () => cancellation.abort('SIGINT');
|
|
96
|
+
const terminate = () => cancellation.abort('SIGTERM');
|
|
97
|
+
const hangup = () => cancellation.abort('SIGHUP');
|
|
98
|
+
byteExecution?.signal.addEventListener('abort', cancel, { once: true });
|
|
99
|
+
process.once('SIGTERM', terminate);
|
|
100
|
+
process.once('SIGINT', cancel);
|
|
101
|
+
process.once('SIGHUP', hangup);
|
|
102
|
+
const deadline = Date.now() + 15_000;
|
|
103
|
+
let requests = 0;
|
|
104
|
+
let retainedBytes = 0;
|
|
105
|
+
const env = {
|
|
106
|
+
HOME: directory, USERPROFILE: directory, XDG_CONFIG_HOME: directory, XDG_CACHE_HOME: directory,
|
|
107
|
+
TMPDIR: directory, TEMP: directory, TMP: directory,
|
|
108
|
+
...(process.platform === 'win32' ? { SystemRoot: process.env.SystemRoot, WINDIR: process.env.WINDIR } : {}),
|
|
109
|
+
};
|
|
110
|
+
const invoke = async (args, format = 'json') => {
|
|
111
|
+
if (format === 'native' && byteExecution) {
|
|
112
|
+
const exitCode = await byteProcess(binary, ['--kubeconfig', config, '--cache-dir', directory, '--request-timeout=0', ...args], env, byteExecution, redactor);
|
|
113
|
+
return { native: true, stdout: '', stderr: '', exitCode };
|
|
114
|
+
}
|
|
115
|
+
if (++requests > 64 || Date.now() >= deadline)
|
|
116
|
+
throw new Failure('UPSTREAM_TIMEOUT', 'The bounded operation exhausted its request or time budget.');
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await execute(binary, ['--kubeconfig', config, '--cache-dir', directory, '--request-timeout=5s', ...args], { env, signal: cancellation.signal, timeout: Math.min(10_000, deadline - Date.now()), maxBuffer: 4 * 1024 * 1024, windowsHide: true });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const failed = error;
|
|
123
|
+
if (failed.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER')
|
|
124
|
+
throw new Failure('OUTPUT_LIMIT', 'Output exceeded 4 MiB; narrow the selection.', 1, { cause: 'byte_limit' });
|
|
125
|
+
if (failed.killed)
|
|
126
|
+
throw new Failure('UPSTREAM_TIMEOUT', 'The bounded request timed out.', 1, { cause: 'timeout' });
|
|
127
|
+
if (format === 'native' && typeof failed.code === 'number')
|
|
128
|
+
return { native: true, stdout: failed.stdout ?? '', stderr: failed.stderr ?? '', exitCode: failed.code };
|
|
129
|
+
const stderr = failed.stderr ?? '';
|
|
130
|
+
const reason = /^Error from server \(([^)]+)\):/m.exec(stderr)?.[1]
|
|
131
|
+
?? (/^error: You must be logged in to the server \(/m.test(stderr) ? 'Unauthorized' : undefined);
|
|
132
|
+
const errors = [
|
|
133
|
+
[['Unauthorized'], 'AUTH_REQUIRED', 401, 'The selected identity was rejected.'],
|
|
134
|
+
[['Forbidden'], 'ACCESS_DENIED', 403, 'The selected identity cannot perform this read.'],
|
|
135
|
+
[['Expired', 'Gone'], 'RESOURCE_EXPIRED', 410, 'The continuation expired; start a new read.'],
|
|
136
|
+
[['Timeout', 'ServerTimeout'], 'UPSTREAM_TIMEOUT', 504, 'The bounded request timed out.'],
|
|
137
|
+
[['NotFound'], args.includes('explain') ? 'SCHEMA_UNAVAILABLE' : args.some(arg => arg.startsWith('--raw=/apis/metrics.k8s.io/')) ? 'API_UNAVAILABLE' : 'RESOURCE_NOT_FOUND', 404, 'The selected resource or API was not found.'],
|
|
138
|
+
];
|
|
139
|
+
for (const [reasons, code, upstreamStatus, message] of errors)
|
|
140
|
+
if (reason && reasons.includes(reason))
|
|
141
|
+
throw new Failure(code, message, 1, { upstreamStatus, cause: code });
|
|
142
|
+
if (!reason) {
|
|
143
|
+
if (/^(?:error: )?(?:Unable to connect to the server: |net\/http: |context )[^\n]*(?:deadline exceeded|timed out|Timeout)/m.test(stderr))
|
|
144
|
+
throw new Failure('UPSTREAM_TIMEOUT', 'The bounded request timed out.', 1, { cause: 'timeout' });
|
|
145
|
+
if (args.includes('explain') && /^error: (?:failed to (?:download|fetch) openapi|error loading schema)/mi.test(stderr)) {
|
|
146
|
+
// Explain loses HTTP failure reasons; a raw read retains the server's status.
|
|
147
|
+
const schema = /^error: failed to fetch openapi schema for ((?:api|apis\/[a-z0-9][a-z0-9.-]*)\/v[0-9]+(?:(?:alpha|beta)[0-9]+)?):/m.exec(stderr)?.[1];
|
|
148
|
+
if (schema) {
|
|
149
|
+
try {
|
|
150
|
+
await invoke(['get', `--raw=/openapi/v3/${schema}`]);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!(error instanceof Failure) || !['RESOURCE_NOT_FOUND', 'UPSTREAM_INVALID'].includes(error.code))
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Failure('SCHEMA_UNAVAILABLE', 'The selected resource schema is unavailable or invalid.', 1, { cause: 'schema' });
|
|
158
|
+
}
|
|
159
|
+
if (/^error: (?:invalid character|unexpected end|[^\n]*cannot unmarshal|invalid JSON)/mi.test(stderr))
|
|
160
|
+
throw new Failure('UPSTREAM_INVALID', 'The API returned a malformed response.', 1, { cause: 'malformed' });
|
|
161
|
+
}
|
|
162
|
+
throw new Failure('UPSTREAM_FAILED', 'The bounded Kubernetes read failed; check endpoint, TLS trust and identity.', 1, { cause: 'transport' });
|
|
163
|
+
}
|
|
164
|
+
retainedBytes += Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr);
|
|
165
|
+
if (retainedBytes > 4 * 1024 * 1024)
|
|
166
|
+
throw new Failure('OUTPUT_LIMIT', 'The operation exceeded its 4 MiB retained-output budget.');
|
|
167
|
+
if (format === 'native')
|
|
168
|
+
return { native: true, stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
|
|
169
|
+
if (format === 'text')
|
|
170
|
+
return result.stdout;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(result.stdout);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
throw new Failure('UPSTREAM_INVALID', 'The API returned malformed JSON.', 1, { cause: 'malformed' });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const raw = (path) => invoke(['get', `--raw=${path}`]);
|
|
179
|
+
let close = async () => { };
|
|
180
|
+
let source = identity?.source ?? '';
|
|
181
|
+
let binding = JSON.stringify(identity);
|
|
182
|
+
try {
|
|
183
|
+
if (clientOnly(invocation)) {
|
|
184
|
+
if (invocation.command.path === 'plugin list')
|
|
185
|
+
env.PATH = process.env.PATH;
|
|
186
|
+
return await invoke(invocation.argv, 'native');
|
|
187
|
+
}
|
|
188
|
+
if (invocation.nativeConfig && invocation.native && invocation.command.path.startsWith('config ')) {
|
|
189
|
+
if (invocation.nativeConfig === 'ambient')
|
|
190
|
+
throw new Failure('CONFIG_INVALID', 'Native config operations require one explicitly selected source file.');
|
|
191
|
+
if (invocation.command.effect === 'localWrite' && /^(true|1)$/i.test(process.env.KUBERNETES_AXI_READ_ONLY ?? ''))
|
|
192
|
+
throw new Failure('READ_ONLY', 'Native config writes are disabled by read-only policy.');
|
|
193
|
+
await writePrivate(config, await readPrivate(invocation.nativeConfig, false));
|
|
194
|
+
await inspectNative(binary, config, redactor, cancellation.signal);
|
|
195
|
+
const result = await invoke(invocation.argv, 'native');
|
|
196
|
+
if (result.exitCode === 0 && invocation.command.effect === 'localWrite')
|
|
197
|
+
await writePrivate(invocation.nativeConfig, await readPrivate(config, true));
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
if (invocation.nativeConfig) {
|
|
201
|
+
const loaded = await nativeConfig(binary, invocation.nativeConfig, flags.get('context'), directory, invocation.trustExec, redactor, cancellation.signal, Boolean(invocation.workflow) || invocation.command.effect === 'clusterWrite');
|
|
202
|
+
close = loaded.close;
|
|
203
|
+
namespace = flags.get('namespace') ?? loaded.namespace;
|
|
204
|
+
source = loaded.source;
|
|
205
|
+
binding = loaded.binding;
|
|
206
|
+
await writePrivate(config, JSON.stringify(loaded.config));
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
await writePrivate(config, JSON.stringify({
|
|
210
|
+
apiVersion: 'v1', kind: 'Config',
|
|
211
|
+
clusters: [{ name: 'selected', cluster: { server: profile.origin, 'certificate-authority-data': Buffer.from(profile.certificateAuthority).toString('base64') } }],
|
|
212
|
+
users: [{ name: 'selected', user }], contexts: [{ name: 'selected', context: { cluster: 'selected', user: 'selected', namespace } }], 'current-context': 'selected',
|
|
213
|
+
}));
|
|
214
|
+
if (invocation.workflow || invocation.command.effect === 'authorization') {
|
|
215
|
+
const session = await mutationSession(config, invocation, namespace, redactor, cancellation.signal, compatibleVersion, binding);
|
|
216
|
+
const closeIdentity = close;
|
|
217
|
+
close = async () => { await session.close(); await closeIdentity(); };
|
|
218
|
+
if (invocation.workflow?.kind === 'confirm' && !session.argv || session.prepared)
|
|
219
|
+
return await session.finish();
|
|
220
|
+
await writePrivate(config, JSON.stringify(session.config));
|
|
221
|
+
const args = [...(session.argv ?? invocation.argv)];
|
|
222
|
+
if (session.rawPath) {
|
|
223
|
+
// RequestURI replaces the configured prefix, so the raw URI must carry it.
|
|
224
|
+
for (let index = 0; index < args.length; index++) {
|
|
225
|
+
if (args[index] === '--raw')
|
|
226
|
+
args[++index] = session.rawPath;
|
|
227
|
+
else if (args[index].startsWith('--raw='))
|
|
228
|
+
args[index] = `--raw=${session.rawPath}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (invocation.workflow && !session.argv) {
|
|
232
|
+
if (!invocation.flags.has('raw') && invocation.command.flags.some(flag => flag.name === 'dry-run'))
|
|
233
|
+
args.push('--dry-run=server');
|
|
234
|
+
if (invocation.command.flags.some(flag => flag.name === 'wait'))
|
|
235
|
+
args.push('--wait=false');
|
|
236
|
+
}
|
|
237
|
+
const result = await invoke(args, 'native');
|
|
238
|
+
if (session.argv) {
|
|
239
|
+
await session.finish(result.exitCode === 0);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
if (invocation.command.effect === 'authorization') {
|
|
243
|
+
const review = await session.finish(result.exitCode === 0);
|
|
244
|
+
process.exitCode = result.exitCode;
|
|
245
|
+
return invocation.native ? result : review;
|
|
246
|
+
}
|
|
247
|
+
if (result.exitCode !== 0) {
|
|
248
|
+
// Expose a guarded refusal before falling back to the native failure.
|
|
249
|
+
await session.finish(false);
|
|
250
|
+
if (invocation.native)
|
|
251
|
+
return result;
|
|
252
|
+
throw new Failure('UPSTREAM_FAILED', 'The operation could not be prepared or validated; check its resource, permissions and arguments.');
|
|
253
|
+
}
|
|
254
|
+
const finished = await session.finish();
|
|
255
|
+
return finished;
|
|
256
|
+
}
|
|
257
|
+
const client = await invoke(['version', '--client=true', '-o', 'json']);
|
|
258
|
+
if (client.clientVersion?.gitVersion !== pins.version)
|
|
259
|
+
throw new Failure('BINARY_VERSION', 'Restore the recorded managed kubectl version.');
|
|
260
|
+
const version = await raw('/version');
|
|
261
|
+
compatibleVersion(version);
|
|
262
|
+
const provenance = { source, kubectlVersion: pins.version, kubectlSha256: digest };
|
|
263
|
+
if (invocation.native)
|
|
264
|
+
return await invoke(invocation.argv, 'native');
|
|
265
|
+
const command = invocation.command.path;
|
|
266
|
+
if (command === 'version')
|
|
267
|
+
return { ...provenance, gitVersion: version.gitVersion };
|
|
268
|
+
if (command === 'logs') {
|
|
269
|
+
const logs = await invoke([...invocation.argv, ...(flags.has('tail') ? [] : ['--tail=100']), ...(flags.has('limit-bytes') ? [] : ['--limit-bytes=65536'])], 'text');
|
|
270
|
+
return { ...provenance, logs, complete: false, truncated: Buffer.byteLength(logs) >= Number(flags.get('limit-bytes') ?? 65536), bound: 'tail and byte limits; snapshot only' };
|
|
271
|
+
}
|
|
272
|
+
if (['describe', 'explain', 'rollout history', 'rollout status'].includes(command))
|
|
273
|
+
return { ...provenance, text: await invoke(invocation.argv, 'text'), complete: false, bound: 'execution and byte limits' };
|
|
274
|
+
const core = await raw('/api');
|
|
275
|
+
const groups = await raw('/apis');
|
|
276
|
+
if (!validCore(core) || !validGroups(groups) || groups.groups.some(group => !group.versions.some(version => version.groupVersion === group.preferredVersion.groupVersion) || group.versions.some(version => !version.groupVersion.startsWith(`${group.name}/`))))
|
|
277
|
+
throw new Failure('UPSTREAM_INVALID', 'API discovery returned an invalid envelope.');
|
|
278
|
+
if (command === 'api-versions')
|
|
279
|
+
return { ...provenance, versions: core.versions, groups: groups.groups.map(group => ({ name: group.name, versions: group.versions.map(version => version.groupVersion), preferredVersion: group.preferredVersion.groupVersion })) };
|
|
280
|
+
const candidates = [...core.versions.map(version => ({ version, prefix: `/api/${version}` })), ...groups.groups.flatMap(group => [group.preferredVersion, ...group.versions.filter(version => version.groupVersion !== group.preferredVersion.groupVersion)].map(({ groupVersion }) => ({ version: groupVersion, prefix: `/apis/${groupVersion}` })))];
|
|
281
|
+
const resources = [];
|
|
282
|
+
const requested = invocation.home ? 'pods' : command.startsWith('top ') ? (command === 'top node' ? 'nodes' : 'pods') : invocation.positionals[0];
|
|
283
|
+
for (const candidate of candidates) {
|
|
284
|
+
if (command.startsWith('top ') && !candidate.version.startsWith('metrics.k8s.io/'))
|
|
285
|
+
continue;
|
|
286
|
+
const listing = await raw(candidate.prefix);
|
|
287
|
+
if (!validDiscovery(listing) || listing.groupVersion !== candidate.version)
|
|
288
|
+
throw new Failure('UPSTREAM_INVALID', 'Resource discovery returned an invalid envelope.');
|
|
289
|
+
for (const resource of listing.resources)
|
|
290
|
+
if (!resource.name.includes('/'))
|
|
291
|
+
resources.push({ ...resource, prefix: candidate.prefix, apiVersion: candidate.version });
|
|
292
|
+
if (command !== 'api-resources' && resources.some(resource => matches(resource, requested)))
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (command === 'api-resources')
|
|
296
|
+
return { ...provenance, resources: resources.map(resource => ({ name: resource.name, aliases: resource.shortNames ?? [], namespaced: resource.namespaced, kind: resource.kind, verbs: resource.verbs, apiVersion: resource.apiVersion })), complete: true };
|
|
297
|
+
const resource = resources.find(resource => matches(resource, requested));
|
|
298
|
+
if (!resource)
|
|
299
|
+
throw new Failure('API_UNAVAILABLE', 'Discovery does not advertise the requested resource.');
|
|
300
|
+
const name = command.startsWith('top ') ? invocation.positionals[0] : invocation.positionals[1];
|
|
301
|
+
if (name && !/^[a-z0-9][a-z0-9.-]*$/.test(name))
|
|
302
|
+
throw new Failure('USAGE', 'Supply one Kubernetes resource name.', 2);
|
|
303
|
+
const route = `${resource.prefix}${resource.namespaced && !all ? `/namespaces/${namespace}` : ''}/${resource.name}${name ? `/${name}` : ''}`;
|
|
304
|
+
const query = new URLSearchParams();
|
|
305
|
+
if (!name && !command.startsWith('top '))
|
|
306
|
+
query.set('limit', '100');
|
|
307
|
+
for (const [flag, parameter] of [['selector', 'labelSelector'], ['field-selector', 'fieldSelector']])
|
|
308
|
+
if (flags.has(flag))
|
|
309
|
+
query.set(parameter, flags.get(flag));
|
|
310
|
+
const body = await raw(`${route}${query.size ? `?${query}` : ''}`);
|
|
311
|
+
let items;
|
|
312
|
+
let page = {};
|
|
313
|
+
if (name) {
|
|
314
|
+
if (!validObject(body) || body.kind !== resource.kind || body.apiVersion !== resource.apiVersion || body.metadata.name !== name)
|
|
315
|
+
throw new Failure('UPSTREAM_INVALID', 'The named response does not match the selected resource.');
|
|
316
|
+
items = [body];
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
if (!validPage(body) || body.kind !== `${resource.kind}List` || body.apiVersion !== resource.apiVersion || !body.items.every(item => validObject(item)))
|
|
320
|
+
throw new Failure('UPSTREAM_INVALID', 'The resource list does not match the discovered schema.');
|
|
321
|
+
items = body.items;
|
|
322
|
+
page = body.metadata;
|
|
323
|
+
}
|
|
324
|
+
if (resource.namespaced && items.some(item => !item.metadata.namespace || (!all && item.metadata.namespace !== namespace)))
|
|
325
|
+
throw new Failure('UPSTREAM_INVALID', 'The response does not match the selected namespace.');
|
|
326
|
+
const rows = items.slice(0, 100).map(item => compact(item, resource.kind));
|
|
327
|
+
const state = { complete: !page.continue && !page.remainingItemCount && items.length <= 100, truncated: items.length > 100, continue: page.continue ?? '', resourceVersion: page.resourceVersion ?? '', ...(page.remainingItemCount === undefined ? {} : { remainingItemCount: page.remainingItemCount }) };
|
|
328
|
+
if (invocation.home)
|
|
329
|
+
return { ...provenance, bin: process.argv[1], description: 'Read Kubernetes with isolated credentials and pinned kubectl.', context: 'selected', namespace, pods: rows, ...state };
|
|
330
|
+
return { ...provenance, [command.startsWith('top ') ? 'metrics' : resource.name]: rows, ...state };
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
cancellation.abort();
|
|
334
|
+
await close();
|
|
335
|
+
await rm(directory, { recursive: true, force: true });
|
|
336
|
+
process.removeListener('SIGTERM', terminate);
|
|
337
|
+
process.removeListener('SIGINT', cancel);
|
|
338
|
+
process.removeListener('SIGHUP', hangup);
|
|
339
|
+
byteExecution?.signal.removeEventListener('abort', cancel);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function matches(resource, requested) {
|
|
343
|
+
return requested !== undefined && [resource.name, resource.singularName, resource.kind.toLowerCase(), ...(resource.shortNames ?? []), `${resource.name}.${resource.apiVersion.split('/')[0]}`].includes(requested);
|
|
344
|
+
}
|
|
345
|
+
function compact(item, kind) {
|
|
346
|
+
const row = { name: item.metadata.name, ...(item.metadata.namespace ? { namespace: item.metadata.namespace } : {}) };
|
|
347
|
+
if (kind === 'Node')
|
|
348
|
+
return { ...row, ready: item.status?.conditions?.find(condition => condition.type === 'Ready')?.status ?? 'Unknown' };
|
|
349
|
+
if (kind === 'Deployment')
|
|
350
|
+
return { ...row, desired: item.spec?.replicas ?? 1, available: item.status?.availableReplicas ?? 0 };
|
|
351
|
+
if (kind === 'Pod' || kind === 'Namespace')
|
|
352
|
+
return { ...row, phase: item.status?.phase ?? 'Unknown' };
|
|
353
|
+
if (kind === 'NodeMetrics' || kind === 'PodMetrics') {
|
|
354
|
+
const usages = kind === 'NodeMetrics' ? [item.usage] : item.containers?.map(container => container.usage);
|
|
355
|
+
if (!usages?.length || usages.some(usage => !usage || typeof usage.cpu !== 'string' || typeof usage.memory !== 'string'))
|
|
356
|
+
throw new Failure('UPSTREAM_INVALID', 'Metrics usage is missing or invalid.');
|
|
357
|
+
return { ...row, cpu: `${usages.reduce((sum, usage) => sum + quantity(usage.cpu, 'cpu'), 0)}m`, memory: `${usages.reduce((sum, usage) => sum + quantity(usage.memory, 'memory'), 0)}Mi` };
|
|
358
|
+
}
|
|
359
|
+
return { ...row, kind };
|
|
360
|
+
}
|
|
361
|
+
function quantity(value, type) {
|
|
362
|
+
const match = /^(\d+(?:\.\d+)?)(n|u|m|Ki|Mi|Gi|Ti|k|M|G|T)?$/.exec(value);
|
|
363
|
+
const units = { '': 1, n: 1e-9, u: 1e-6, m: 1e-3, Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4, k: 1000, M: 1e6, G: 1e9, T: 1e12 };
|
|
364
|
+
if (!match)
|
|
365
|
+
throw new Failure('UPSTREAM_INVALID', 'Metrics contain an invalid quantity.');
|
|
366
|
+
return Number((Number(match[1]) * units[match[2] ?? ''] * (type === 'cpu' ? 1000 : 1 / 1024 ** 2)).toPrecision(12));
|
|
367
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, posix } from 'node:path';
|
|
3
|
+
import { Failure } from './errors.js';
|
|
4
|
+
const fileLimit = 100 * 1024 * 1024;
|
|
5
|
+
const totalLimit = 250 * 1024 * 1024;
|
|
6
|
+
export function relativeFile(path) {
|
|
7
|
+
if (!path || /[\\:\x00-\x1f\x7f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..'))
|
|
8
|
+
throw new Failure('PATH_REFUSED', 'Use regular files at canonical relative workspace paths.');
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
export async function workspaceTar(root, path, name = posix.basename(path)) {
|
|
12
|
+
relativeFile(path);
|
|
13
|
+
relativeFile(name);
|
|
14
|
+
const parts = path.split('/');
|
|
15
|
+
for (let count = 1; count <= parts.length; count++) {
|
|
16
|
+
if ((await lstat(join(root, ...parts.slice(0, count)))).isSymbolicLink())
|
|
17
|
+
throw new Failure('PATH_REFUSED', 'Workspace links are refused.');
|
|
18
|
+
}
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let count = 0;
|
|
21
|
+
let total = 0;
|
|
22
|
+
const add = async (source, name) => {
|
|
23
|
+
const info = await lstat(source);
|
|
24
|
+
if (info.isSymbolicLink() || !info.isDirectory() && !info.isFile())
|
|
25
|
+
throw new Failure('PATH_REFUSED', 'Workspace transfers accept only regular files and directories.');
|
|
26
|
+
if (info.isDirectory()) {
|
|
27
|
+
for (const child of (await readdir(source)).sort())
|
|
28
|
+
await add(join(source, child), `${name}/${child}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (++count > 100 || info.size > fileLimit || (total += info.size) > totalLimit)
|
|
32
|
+
throw new Failure('TRANSFER_LIMIT', 'The workspace exceeds its file count or byte limits.');
|
|
33
|
+
const bytes = await readFile(source);
|
|
34
|
+
if (bytes.length !== info.size)
|
|
35
|
+
throw new Failure('WORKSPACE_CHANGED', 'A workspace file changed during preparation.');
|
|
36
|
+
const header = Buffer.alloc(512);
|
|
37
|
+
const separator = name.lastIndexOf('/');
|
|
38
|
+
const leaf = Buffer.byteLength(name) <= 100 ? name : name.slice(separator + 1);
|
|
39
|
+
const prefix = Buffer.byteLength(name) <= 100 ? '' : name.slice(0, separator);
|
|
40
|
+
if (Buffer.byteLength(leaf) > 100 || Buffer.byteLength(prefix) > 155 || separator < 0 && prefix)
|
|
41
|
+
throw new Failure('PATH_REFUSED', 'The file path exceeds the portable tar header limits.');
|
|
42
|
+
header.write(leaf, 0, 100);
|
|
43
|
+
header.write(prefix, 345, 155);
|
|
44
|
+
const octal = (value, offset, length) => { header.write(value.toString(8).padStart(length - 1, '0') + '\0', offset, length); };
|
|
45
|
+
octal(0o600, 100, 8);
|
|
46
|
+
octal(0, 108, 8);
|
|
47
|
+
octal(0, 116, 8);
|
|
48
|
+
octal(bytes.length, 124, 12);
|
|
49
|
+
octal(0, 136, 12);
|
|
50
|
+
header.fill(32, 148, 156);
|
|
51
|
+
header[156] = 48;
|
|
52
|
+
header.write('ustar\0', 257);
|
|
53
|
+
header.write('00', 263);
|
|
54
|
+
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
|
55
|
+
header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8);
|
|
56
|
+
chunks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
57
|
+
};
|
|
58
|
+
await add(join(root, path), name);
|
|
59
|
+
return Buffer.concat([...chunks, Buffer.alloc(1024)]);
|
|
60
|
+
}
|
|
61
|
+
export function readWorkspaceTar(archive) {
|
|
62
|
+
if (archive.length > totalLimit + 102400)
|
|
63
|
+
throw new Failure('TRANSFER_LIMIT', 'The archive exceeds the workspace limit.');
|
|
64
|
+
const files = [];
|
|
65
|
+
const names = new Set();
|
|
66
|
+
let total = 0;
|
|
67
|
+
let offset = 0;
|
|
68
|
+
const number = (header, start, length) => {
|
|
69
|
+
const text = header.subarray(start, start + length).toString('ascii').replace(/\0.*$/, '').trim();
|
|
70
|
+
if (!/^[0-7]+$/.test(text))
|
|
71
|
+
throw new Failure('ARCHIVE_REFUSED', 'The archive contains an invalid numeric header.');
|
|
72
|
+
return Number.parseInt(text, 8);
|
|
73
|
+
};
|
|
74
|
+
const field = (header, start, length) => header.subarray(start, start + length).toString('utf8').replace(/\0.*$/, '');
|
|
75
|
+
while (offset + 512 <= archive.length) {
|
|
76
|
+
const header = archive.subarray(offset, offset + 512);
|
|
77
|
+
if (header.every(byte => byte === 0)) {
|
|
78
|
+
if (!archive.subarray(offset).every(byte => byte === 0))
|
|
79
|
+
throw new Failure('ARCHIVE_REFUSED', 'The archive has trailing nonzero data.');
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
const expected = number(header, 148, 8);
|
|
83
|
+
const checksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
84
|
+
if (checksum !== expected)
|
|
85
|
+
throw new Failure('ARCHIVE_REFUSED', 'The archive checksum is invalid.');
|
|
86
|
+
const prefix = field(header, 345, 155);
|
|
87
|
+
const name = relativeFile((prefix ? `${prefix}/` : '') + field(header, 0, 100).replace(/\/$/, ''));
|
|
88
|
+
const size = number(header, 124, 12);
|
|
89
|
+
const type = header[156];
|
|
90
|
+
if (![0, 48, 53].includes(type))
|
|
91
|
+
throw new Failure('PATH_REFUSED', 'Links, extended headers and special archive entries are refused.');
|
|
92
|
+
if (size > fileLimit || (total += size) > totalLimit || offset + 512 + size > archive.length)
|
|
93
|
+
throw new Failure('TRANSFER_LIMIT', 'The archive exceeds its declared or permitted size.');
|
|
94
|
+
if (names.has(name) || [...names].some(other => other.startsWith(name + '/') || name.startsWith(other + '/')))
|
|
95
|
+
throw new Failure('PATH_REFUSED', 'The archive has duplicate or conflicting paths.');
|
|
96
|
+
if (type === 53) {
|
|
97
|
+
if (size !== 0)
|
|
98
|
+
throw new Failure('ARCHIVE_REFUSED', 'Directory entries must have no payload.');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (files.length >= 100)
|
|
102
|
+
throw new Failure('TRANSFER_LIMIT', 'The archive exceeds 100 files.');
|
|
103
|
+
names.add(name);
|
|
104
|
+
files.push({ path: name, bytes: Buffer.from(archive.subarray(offset + 512, offset + 512 + size)) });
|
|
105
|
+
}
|
|
106
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
107
|
+
}
|
|
108
|
+
throw new Failure('ARCHIVE_REFUSED', 'The archive ended before its EOF marker.');
|
|
109
|
+
}
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Optional session guidance
|
|
2
|
+
|
|
3
|
+
Read Kubernetes resources using isolated credentials and pinned kubectl v1.32.2.
|
|
4
|
+
Use --profile <name> to select an AXI identity.
|
|
5
|
+
Use native -- <kubectl arguments> for native output; native --profile is upstream profiling and is blocked because it writes files.
|
|
6
|
+
Reads are bounded; inspect complete, truncated and continue before treating output as complete.
|
|
7
|
+
Writes default off.
|
|
8
|
+
Explicitly enable KUBERNETES_AXI_READ_ONLY=false, then use prepare -- <mutation>, preview <request-key>, and confirm <request-key> --digest <preview-digest>.
|
|
9
|
+
Preparation reads exact targets; local preview makes no API calls.
|
|
10
|
+
Confirmation is one-shot and refuses stale or changed targets.
|
|
11
|
+
Bulk deletion also requires KUBERNETES_AXI_COLLECTION_DELETE=true.
|
|
12
|
+
Use server-dry-run -- <mutation> for explicit API validation without persistence.
|
|
13
|
+
Exec/attach and named debug mutations require preparation and confirmation.
|
|
14
|
+
In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects.
|
|
15
|
+
Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts.
|
|
16
|
+
Bounded noninteractive execution and existing hosted bounded sessions remain supported.
|
|
17
|
+
Streaming redaction and interactive execution redesigns are tracked at https://github.com/knowttl/kubernetes-axi/issues/34 and https://github.com/knowttl/kubernetes-axi/issues/35.
|
|
18
|
+
Native Windows support remains pending.
|
|
19
|
+
Token issuance additionally requires KUBERNETES_AXI_ACQUIRE_CREDENTIAL=true, a duration of at most one hour, and --credential-fd <inherited-private-pipe> before confirm.
|
|
20
|
+
Issued tokens are delivered once and never retained.
|
|
21
|
+
Hosted workspace transfer, byte sessions, followed logs, watches and multi-port forwarding use caller-owned authenticated Artium sessions.
|
|
22
|
+
In 0.1.x, run editors, plugins and external render/diff helpers outside the CLI, then prepare the resulting manifest or patch and confirm its preview.
|
|
23
|
+
Their execution inside the CLI is refused before effects.
|
|
24
|
+
Generic proxy listeners and credential overrides remain refused.
|
|
25
|
+
Native completion, options, plugin list and version --client=true work without credentials.
|
|
26
|
+
In 0.1.x, run diff and its external helpers outside the CLI.
|
|
27
|
+
Submit resulting changes through prepare, preview and confirm.
|
|
28
|
+
Run kubernetes-axi --help for available commands and kubernetes-axi native -- <command> --help for generated upstream flags.
|
|
29
|
+
|
|
30
|
+
Install a session hook only with explicit user opt-in.
|
|
31
|
+
Claude Code and Codex SessionStart hooks can run `kubernetes-axi` for the bounded home view.
|
|
32
|
+
OpenCode can inject the same home view through a user-managed plugin.
|
|
33
|
+
Use a PATH-verified binary or the absolute installed executable path.
|
|
34
|
+
No hook is installed by package installation, CLI reads or tests.
|
|
35
|
+
The installable skill is an alternative when session hooks are unavailable.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "v1.32.2",
|
|
3
|
+
"supportedServerMinors": [31, 32, 33],
|
|
4
|
+
"matrixStatus": "observational until live commissioning",
|
|
5
|
+
"decision": "kubectl pin v1.32.2 - captain-delegated (overnight), plan-named selection; revisit at commissioning.",
|
|
6
|
+
"binaries": {
|
|
7
|
+
"linux-x64": {
|
|
8
|
+
"file": "kubectl-linux-amd64",
|
|
9
|
+
"url": "https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl",
|
|
10
|
+
"sha256": "4f6a959dcc5b702135f8354cc7109b542a2933c46b808b248a214c1f69f817ea"
|
|
11
|
+
},
|
|
12
|
+
"win32-x64": {
|
|
13
|
+
"file": "kubectl-windows-amd64.exe",
|
|
14
|
+
"url": "https://dl.k8s.io/release/v1.32.2/bin/windows/amd64/kubectl.exe",
|
|
15
|
+
"sha256": "cf51a1c6bf3b6ba6a5b549d1debf8aa6afb00c4c5a3d5d4bb1072f54cbe4390f"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowttl/kubernetes-axi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-facing Kubernetes CLI backed by pinned kubectl",
|
|
5
|
+
"repository": { "type": "git", "url": "git+https://github.com/knowttl/kubernetes-axi.git" },
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": { "node": "24.14.0", "npm": "11.9.0" },
|
|
8
|
+
"packageManager": "npm@11.9.0",
|
|
9
|
+
"publishConfig": { "access": "public" },
|
|
10
|
+
"bin": { "kubernetes-axi": "bin/kubernetes-axi.js" },
|
|
11
|
+
"files": ["bin", "dist", "vendor", "third-party", "THIRD_PARTY_NOTICES.md", "axi-credential.schema.json", "kubectl-pins.json", "command-metadata.json", "skills", "docs/hooks.md"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"build": "node scripts/check-binaries.mjs && tsc",
|
|
15
|
+
"lint": "tsc --noEmit && tsc -p tsconfig.tools.json && tsc -p tsconfig.verify.json",
|
|
16
|
+
"test": "unshare --user --map-root-user --net -- sh -c 'ip link set lo up && GOPROXY=off node scripts/generate-metadata.ts --check && GOPROXY=off node scripts/generate-protobuf.ts --check && python3 scripts/verify.py task B1.1 --mode offline --pack evidence && node test/private-files.ts && node test/b2.ts && node test/package-contract.ts'",
|
|
17
|
+
"generate:metadata": "node scripts/generate-metadata.ts",
|
|
18
|
+
"check:metadata": "GOPROXY=off node scripts/generate-metadata.ts --check",
|
|
19
|
+
"check": "npm run lint && npm test"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": { "@toon-format/toon": "4.4.0", "@types/node": "24.10.0", "ajv": "8.17.1", "protobufjs": "7.6.6" },
|
|
22
|
+
"devDependencies": { "typescript": "5.9.3" }
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kubernetes-axi
|
|
3
|
+
description: Inspect Kubernetes resources, discovery, metrics and logs with bounded output and isolated credentials.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read Kubernetes resources using isolated credentials and pinned kubectl v1.32.2.
|
|
7
|
+
Use --profile <name> to select an AXI identity.
|
|
8
|
+
Use native -- <kubectl arguments> for native output; native --profile is upstream profiling and is blocked because it writes files.
|
|
9
|
+
Reads are bounded; inspect complete, truncated and continue before treating output as complete.
|
|
10
|
+
Writes default off.
|
|
11
|
+
Explicitly enable KUBERNETES_AXI_READ_ONLY=false, then use prepare -- <mutation>, preview <request-key>, and confirm <request-key> --digest <preview-digest>.
|
|
12
|
+
Preparation reads exact targets; local preview makes no API calls.
|
|
13
|
+
Confirmation is one-shot and refuses stale or changed targets.
|
|
14
|
+
Bulk deletion also requires KUBERNETES_AXI_COLLECTION_DELETE=true.
|
|
15
|
+
Use server-dry-run -- <mutation> for explicit API validation without persistence.
|
|
16
|
+
Exec/attach and named debug mutations require preparation and confirmation.
|
|
17
|
+
In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects.
|
|
18
|
+
Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts.
|
|
19
|
+
Bounded noninteractive execution and existing hosted bounded sessions remain supported.
|
|
20
|
+
Streaming redaction and interactive execution redesigns are tracked at https://github.com/knowttl/kubernetes-axi/issues/34 and https://github.com/knowttl/kubernetes-axi/issues/35.
|
|
21
|
+
Native Windows support remains pending.
|
|
22
|
+
Token issuance additionally requires KUBERNETES_AXI_ACQUIRE_CREDENTIAL=true, a duration of at most one hour, and --credential-fd <inherited-private-pipe> before confirm.
|
|
23
|
+
Issued tokens are delivered once and never retained.
|
|
24
|
+
Hosted workspace transfer, byte sessions, followed logs, watches and multi-port forwarding use caller-owned authenticated Artium sessions.
|
|
25
|
+
In 0.1.x, run editors, plugins and external render/diff helpers outside the CLI, then prepare the resulting manifest or patch and confirm its preview.
|
|
26
|
+
Their execution inside the CLI is refused before effects.
|
|
27
|
+
Generic proxy listeners and credential overrides remain refused.
|
|
28
|
+
Native completion, options, plugin list and version --client=true work without credentials.
|
|
29
|
+
In 0.1.x, run diff and its external helpers outside the CLI.
|
|
30
|
+
Submit resulting changes through prepare, preview and confirm.
|
|
31
|
+
Run npx -y @knowttl/kubernetes-axi --help for available commands and npx -y @knowttl/kubernetes-axi native -- <command> --help for generated upstream flags.
|
|
32
|
+
|