@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.
Files changed (48) hide show
  1. package/README.md +306 -2
  2. package/THIRD_PARTY_NOTICES.md +13 -0
  3. package/axi-credential.schema.json +31 -0
  4. package/bin/kubernetes-axi.js +10 -0
  5. package/command-metadata.json +36947 -0
  6. package/dist/byte-process.d.ts +9 -0
  7. package/dist/byte-process.js +56 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +68 -0
  10. package/dist/credentials.d.ts +28 -0
  11. package/dist/credentials.js +113 -0
  12. package/dist/errors.d.ts +12 -0
  13. package/dist/errors.js +11 -0
  14. package/dist/exec-shim.d.ts +1 -0
  15. package/dist/exec-shim.js +16 -0
  16. package/dist/grammar.d.ts +50 -0
  17. package/dist/grammar.js +248 -0
  18. package/dist/hosted-session.d.ts +1 -0
  19. package/dist/hosted-session.js +201 -0
  20. package/dist/metadata-pin.d.ts +3 -0
  21. package/dist/metadata-pin.js +4 -0
  22. package/dist/mutations.d.ts +11 -0
  23. package/dist/mutations.js +526 -0
  24. package/dist/native.d.ts +26 -0
  25. package/dist/native.js +143 -0
  26. package/dist/private-files.d.ts +9 -0
  27. package/dist/private-files.js +137 -0
  28. package/dist/protobuf-schema.d.ts +2 -0
  29. package/dist/protobuf-schema.js +1 -0
  30. package/dist/protobuf.d.ts +1 -0
  31. package/dist/protobuf.js +67 -0
  32. package/dist/redaction.d.ts +10 -0
  33. package/dist/redaction.js +50 -0
  34. package/dist/remote-session.d.ts +22 -0
  35. package/dist/remote-session.js +90 -0
  36. package/dist/route-policy.d.ts +1 -0
  37. package/dist/route-policy.js +19 -0
  38. package/dist/runner.d.ts +11 -0
  39. package/dist/runner.js +367 -0
  40. package/dist/workspace-tar.d.ts +6 -0
  41. package/dist/workspace-tar.js +109 -0
  42. package/docs/hooks.md +35 -0
  43. package/kubectl-pins.json +18 -0
  44. package/package.json +21 -4
  45. package/skills/kubernetes-axi/SKILL.md +32 -0
  46. package/third-party/kubectl-LICENSE.txt +202 -0
  47. package/vendor/kubectl-linux-amd64 +4 -0
  48. package/vendor/kubectl-windows-amd64.exe +4 -0
package/dist/native.js ADDED
@@ -0,0 +1,143 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createServer } from 'node:net';
3
+ import { chmod, rm } from 'node:fs/promises';
4
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+ import { Failure } from './errors.js';
8
+ import { readPrivate, writePrivate } from './private-files.js';
9
+ const execute = promisify(execFile);
10
+ export async function inspectNative(binary, snapshot, redactor, signal) {
11
+ try {
12
+ const result = await execute(binary, ['--kubeconfig', snapshot, 'config', 'view', '--merge=false', '--raw', '-o', 'json'], { signal, timeout: 5000, maxBuffer: 4 * 1024 * 1024 });
13
+ const config = JSON.parse(result.stdout);
14
+ for (const { user } of config.users ?? [])
15
+ if (user) {
16
+ redactor.register(...['token', 'password'].map(key => typeof user[key] === 'string' ? user[key] : undefined));
17
+ for (const key of ['client-key-data', 'client-certificate-data'])
18
+ if (typeof user[key] === 'string')
19
+ redactor.register(Buffer.from(user[key], 'base64').toString());
20
+ const helper = user.exec;
21
+ if (helper)
22
+ redactor.register(...(helper.env ?? []).map(item => item.value));
23
+ }
24
+ return config;
25
+ }
26
+ catch {
27
+ throw new Failure('CONFIG_INVALID', 'The selected native kubeconfig is invalid.');
28
+ }
29
+ }
30
+ export async function nativeConfig(binary, selection, context, directory, trustExec, redactor, signal, resourceMutation) {
31
+ const paths = selection === 'ambient' ? (process.env.KUBECONFIG ? process.env.KUBECONFIG.split(delimiter).filter(Boolean) : [join((await import('node:os')).homedir(), '.kube', 'config')]) : [selection];
32
+ if (!paths.length || paths.some(path => !isAbsolute(path)))
33
+ throw new Failure('CONFIG_INVALID', 'Select absolute native kubeconfig paths.');
34
+ const merged = { clusters: [], users: [], contexts: [] };
35
+ const origins = new Map();
36
+ for (const [index, path] of paths.entries()) {
37
+ let contents;
38
+ try {
39
+ contents = await readPrivate(path, false);
40
+ }
41
+ catch (error) {
42
+ if (selection === 'ambient' && error.code === 'ENOENT')
43
+ continue;
44
+ throw error;
45
+ }
46
+ const snapshot = join(directory, `source-${index}.json`);
47
+ await writePrivate(snapshot, contents);
48
+ let parsed;
49
+ try {
50
+ parsed = await inspectNative(binary, snapshot, redactor, signal);
51
+ }
52
+ finally {
53
+ await rm(snapshot, { force: true });
54
+ }
55
+ for (const key of ['clusters', 'users', 'contexts']) {
56
+ for (const entry of parsed[key] ?? [])
57
+ if (!merged[key].some(existing => existing.name === entry.name)) {
58
+ merged[key].push(entry);
59
+ origins.set(entry, path);
60
+ }
61
+ }
62
+ if (!merged['current-context'] && parsed['current-context'])
63
+ merged['current-context'] = parsed['current-context'];
64
+ }
65
+ const name = context ?? merged['current-context'];
66
+ const selected = merged.contexts.find(entry => entry.name === name)?.context;
67
+ const clusterEntry = merged.clusters.find(entry => entry.name === selected?.cluster);
68
+ const userEntry = merged.users.find(entry => entry.name === selected?.user);
69
+ if (!selected || !clusterEntry?.cluster || !userEntry?.user)
70
+ throw new Failure('CONFIG_INVALID', 'Select an existing native context with a cluster and identity.');
71
+ const cluster = { ...clusterEntry.cluster };
72
+ const user = { ...userEntry.user };
73
+ if (user.exec && resourceMutation)
74
+ throw new Failure('AUTH_HELPER_MUTATION_UNAVAILABLE', 'ExecCredential helper identities cannot prepare, confirm or server-dry-run resource mutations until principal binding is supported. Use a static token or client-certificate identity.');
75
+ if (['as', 'as-uid', 'as-groups', 'as-user-extra'].some(field => Object.hasOwn(user, field)))
76
+ throw new Failure('USAGE', 'Native kubeconfig impersonation is unavailable under the isolated bounded-read policy (impersonation).', 2);
77
+ const server = new URL(String(cluster.server));
78
+ if (server.protocol !== 'https:' || server.username || server.password || cluster['insecure-skip-tls-verify'] || cluster['proxy-url'])
79
+ throw new Failure('CONFIG_INVALID', 'Native contexts require verified HTTPS without a proxy override.');
80
+ if (user['auth-provider'] || user.tokenFile || user.username || user.password)
81
+ throw new Failure('CONFIG_INVALID', 'Use native token, certificate or a trusted ExecCredential helper.');
82
+ for (const [target, entry, field, secret] of [[cluster, clusterEntry, 'certificate-authority', false], [user, userEntry, 'client-certificate', false], [user, userEntry, 'client-key', true]]) {
83
+ if (target[field] && !target[`${field}-data`])
84
+ target[`${field}-data`] = Buffer.from(await readPrivate(resolve(dirname(origins.get(entry)), String(target[field])), secret)).toString('base64');
85
+ delete target[field];
86
+ }
87
+ if (!cluster['certificate-authority-data'])
88
+ throw new Failure('CONFIG_INVALID', 'Supply explicit native certificate authority trust.');
89
+ if (!user.exec && !user.token && !(user['client-certificate-data'] && user['client-key-data']))
90
+ throw new Failure('AUTH_REQUIRED', 'Supply authentication in the selected native identity.');
91
+ redactor.register(typeof user.token === 'string' ? user.token : undefined, ...['client-key-data', 'client-certificate-data'].map(key => typeof user[key] === 'string' ? Buffer.from(user[key], 'base64').toString() : undefined));
92
+ const binding = JSON.stringify({ cluster, user, context: name, namespace: selected.namespace ?? 'default' });
93
+ let close = async () => { };
94
+ if (user.exec) {
95
+ if (!trustExec)
96
+ throw new Failure('AUTH_HELPER_UNTRUSTED', 'Review the selected helper, then opt in with --trust-exec. Its existing cache remains helper-owned.');
97
+ if (process.platform === 'win32')
98
+ throw new Failure('AUTH_HELPER_UNAVAILABLE', 'Native Windows helper-channel commissioning is pending.');
99
+ const helper = user.exec;
100
+ if (!['client.authentication.k8s.io/v1', 'client.authentication.k8s.io/v1beta1'].includes(helper.apiVersion) || typeof helper.command !== 'string' || (helper.args && (!Array.isArray(helper.args) || helper.args.some(arg => typeof arg !== 'string'))) || !['Never', 'IfAvailable', undefined].includes(helper.interactiveMode))
101
+ throw new Failure('AUTH_HELPER_UNAVAILABLE', 'Select a supported non-interactive ExecCredential helper.');
102
+ const command = helper.command.includes('/') ? resolve(dirname(origins.get(userEntry)), helper.command) : helper.command;
103
+ const socketPath = join(directory, 'exec.sock');
104
+ let acquisitions = 0;
105
+ const server = createServer({ allowHalfOpen: true }, socket => {
106
+ let request = '';
107
+ socket.setEncoding('utf8');
108
+ socket.on('error', () => { });
109
+ socket.on('data', chunk => { request += chunk; if (request.length > 1024 * 1024)
110
+ socket.destroy(); });
111
+ socket.on('end', () => {
112
+ void (async () => {
113
+ try {
114
+ if (++acquisitions > 64)
115
+ throw new Error('bounded');
116
+ const input = JSON.parse(request);
117
+ const env = { ...process.env, ...Object.fromEntries((helper.env ?? []).map(item => [item.name, item.value])), KUBERNETES_EXEC_INFO: input.info };
118
+ const result = await execute(command, helper.args ?? [], { env, signal, timeout: 8000, maxBuffer: 1024 * 1024 });
119
+ const credential = JSON.parse(result.stdout);
120
+ const status = credential.status;
121
+ if (credential.apiVersion !== helper.apiVersion || credential.kind !== 'ExecCredential' || !status || !((typeof status.token === 'string' && status.token.length > 0) || (typeof status.clientCertificateData === 'string' && typeof status.clientKeyData === 'string')) || (status.expirationTimestamp && !Number.isFinite(Date.parse(status.expirationTimestamp))))
122
+ throw new Error('invalid');
123
+ redactor.register(status.token, status.clientKeyData, status.clientCertificateData);
124
+ // Nothing reaches kubectl until every newly acquired secret is registered.
125
+ socket.end(JSON.stringify({ apiVersion: credential.apiVersion, kind: 'ExecCredential', status }));
126
+ }
127
+ catch {
128
+ socket.end();
129
+ }
130
+ })();
131
+ });
132
+ });
133
+ await new Promise((done, reject) => { server.once('error', reject); server.listen(socketPath, done); });
134
+ await chmod(socketPath, 0o600);
135
+ close = async () => { await new Promise(done => server.close(() => done())); };
136
+ user.exec = { apiVersion: helper.apiVersion, command: process.execPath, args: [fileURLToPath(new URL('./exec-shim.js', import.meta.url)), socketPath], interactiveMode: helper.interactiveMode ?? 'Never', provideClusterInfo: helper.provideClusterInfo ?? false };
137
+ }
138
+ return {
139
+ binding,
140
+ config: { apiVersion: 'v1', kind: 'Config', clusters: [{ name: 'selected', cluster }], users: [{ name: 'selected', user }], contexts: [{ name: name, context: { cluster: 'selected', user: 'selected', namespace: selected.namespace ?? 'default' } }], 'current-context': name },
141
+ source: `native:${paths.join(delimiter)} context:${name}; ambient exception; helper cache owned by helper`, namespace: selected.namespace ?? 'default', close,
142
+ };
143
+ }
@@ -0,0 +1,9 @@
1
+ export interface PrivateFileOptions {
2
+ platform?: NodeJS.Platform;
3
+ restrictAcl?: (path: string) => void;
4
+ verifyAcl?: (path: string) => void;
5
+ diagnostic?: boolean;
6
+ }
7
+ export declare function readPrivate(path: string, secret: boolean, options?: PrivateFileOptions): Promise<string>;
8
+ export declare function privateDirectory(path: string, options?: PrivateFileOptions): Promise<void>;
9
+ export declare function writePrivate(path: string, content: string, options?: PrivateFileOptions): Promise<void>;
@@ -0,0 +1,137 @@
1
+ import { constants } from 'node:fs';
2
+ import { open, lstat, mkdir, chmod, rename, rm } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { setTimeout } from 'node:timers/promises';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { Failure } from './errors.js';
8
+ export async function readPrivate(path, secret, options = {}) {
9
+ const platform = options.platform ?? process.platform;
10
+ await unredirected(path);
11
+ const metadata = await lstat(path);
12
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
13
+ throw new Failure('CONFIG_INSECURE', `Use a regular, unredirected credential file: ${path}.`);
14
+ }
15
+ if (platform === 'win32' && options.diagnostic)
16
+ (options.verifyAcl ?? verifyWindows)(path);
17
+ const handle = await open(path, constants.O_RDONLY | (platform === 'win32' ? 0 : constants.O_NOFOLLOW));
18
+ try {
19
+ const stat = await handle.stat();
20
+ if (stat.dev !== metadata.dev || stat.ino !== metadata.ino || !stat.isFile() ||
21
+ (platform !== 'win32' && (stat.uid !== process.getuid?.() || (stat.mode & (secret ? 0o077 : 0o022)) !== 0))) {
22
+ throw new Failure('CONFIG_INSECURE', `Credential file must be owned by the current user; use chmod 600 ${path}.`);
23
+ }
24
+ return await handle.readFile('utf8');
25
+ }
26
+ finally {
27
+ await handle.close();
28
+ }
29
+ }
30
+ function verifyWindows(path) {
31
+ try {
32
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
33
+ '$ErrorActionPreference="Stop"; $acl=Get-Acl -LiteralPath $env:KUBERNETES_AXI_ACL_PATH; ' +
34
+ '$ids=@([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value,"S-1-5-18","S-1-5-32-544"); ' +
35
+ 'if(!$acl.AreAccessRulesProtected) { throw "Unprotected" }; ' +
36
+ 'foreach($rule in $acl.Access) { $sid=$rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value; ' +
37
+ 'if($rule.AccessControlType -ne "Allow" -or $sid -notin $ids) { throw "Unexpected access" } }; ' +
38
+ 'foreach($sid in $ids) { if(!($acl.Access | Where-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid -and ($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl })) { throw "Missing access" } }'], { env: { ...process.env, KUBERNETES_AXI_ACL_PATH: path }, stdio: 'ignore' });
39
+ }
40
+ catch {
41
+ throw new Failure('CONFIG_INSECURE', `Re-run the tool private write to protect the Windows ACL: ${path}.`);
42
+ }
43
+ }
44
+ function restrictWindows(path) {
45
+ // Paths travel through the child environment, never interpolated into PowerShell code.
46
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
47
+ '$ErrorActionPreference="Stop"; $p=$env:KUBERNETES_AXI_ACL_PATH; ' +
48
+ '$acl=Get-Acl -LiteralPath $p; $acl.SetAccessRuleProtection($true,$false); ' +
49
+ '$acl.Access | ForEach-Object { [void]$acl.RemoveAccessRuleSpecific($_) }; ' +
50
+ '$ids=@([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value,"S-1-5-18","S-1-5-32-544"); ' +
51
+ 'foreach($id in $ids) { $sid=New-Object System.Security.Principal.SecurityIdentifier($id); ' +
52
+ '$rule=if((Get-Item -LiteralPath $p).PSIsContainer) { New-Object System.Security.AccessControl.FileSystemAccessRule($sid,"FullControl","ContainerInherit,ObjectInherit","None","Allow") } else { New-Object System.Security.AccessControl.FileSystemAccessRule($sid,"FullControl","Allow") }; ' +
53
+ '$acl.AddAccessRule($rule) }; Set-Acl -LiteralPath $p -AclObject $acl'], { env: { ...process.env, KUBERNETES_AXI_ACL_PATH: path }, stdio: 'ignore' });
54
+ }
55
+ export async function privateDirectory(path, options = {}) {
56
+ const platform = options.platform ?? process.platform;
57
+ await unredirected(path, true);
58
+ const missing = [];
59
+ for (let current = path;; current = dirname(current)) {
60
+ try {
61
+ await lstat(current);
62
+ break;
63
+ }
64
+ catch (error) {
65
+ if (error.code !== 'ENOENT')
66
+ throw error;
67
+ missing.push(current);
68
+ }
69
+ }
70
+ for (const directory of missing.reverse()) {
71
+ await mkdir(directory, { mode: 0o700 });
72
+ if (platform === 'win32')
73
+ (options.restrictAcl ?? restrictWindows)(directory);
74
+ }
75
+ if (platform !== 'win32') {
76
+ if ((await lstat(path)).uid !== process.getuid?.())
77
+ throw new Failure('CONFIG_INSECURE', 'Use a directory owned by the current user.');
78
+ await chmod(path, 0o700);
79
+ }
80
+ if (platform === 'win32' && !missing.includes(path))
81
+ (options.restrictAcl ?? restrictWindows)(path);
82
+ }
83
+ export async function writePrivate(path, content, options = {}) {
84
+ const platform = options.platform ?? process.platform;
85
+ await privateDirectory(dirname(path), options);
86
+ await unredirected(path, true);
87
+ try {
88
+ await readPrivate(path, true, options);
89
+ }
90
+ catch (error) {
91
+ if (error.code !== 'ENOENT')
92
+ throw error;
93
+ }
94
+ const temporary = join(dirname(path), `.private-${randomUUID()}`);
95
+ try {
96
+ const handle = await open(temporary, 'wx', 0o600);
97
+ try {
98
+ if (platform === 'win32')
99
+ (options.restrictAcl ?? restrictWindows)(temporary);
100
+ await handle.writeFile(content);
101
+ await handle.sync();
102
+ }
103
+ finally {
104
+ await handle.close();
105
+ }
106
+ const deadline = Date.now() + 1000;
107
+ for (;;) {
108
+ try {
109
+ await rename(temporary, path);
110
+ break;
111
+ }
112
+ catch (error) {
113
+ if (platform !== 'win32' || !['EPERM', 'EBUSY', 'EACCES'].includes(error.code ?? '') || Date.now() >= deadline)
114
+ throw error;
115
+ await setTimeout(50);
116
+ }
117
+ }
118
+ }
119
+ finally {
120
+ await rm(temporary, { force: true });
121
+ }
122
+ }
123
+ async function unredirected(path, missing = false) {
124
+ for (let current = path;; current = dirname(current)) {
125
+ try {
126
+ const stat = await lstat(current);
127
+ if (stat.isSymbolicLink())
128
+ throw new Failure('CONFIG_INSECURE', 'Use an unredirected credential path; remove symbolic links or junctions.');
129
+ }
130
+ catch (error) {
131
+ if (!missing || error.code !== 'ENOENT')
132
+ throw error;
133
+ }
134
+ if (dirname(current) === current)
135
+ break;
136
+ }
137
+ }
@@ -0,0 +1,2 @@
1
+ import type { INamespace } from 'protobufjs';
2
+ export declare const protobufSchema: INamespace;