@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, open, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { join, posix, relative } from 'node:path';
|
|
6
|
+
import { identity, validateEnvironment } from './credentials.js';
|
|
7
|
+
import { Failure } from './errors.js';
|
|
8
|
+
import { parse } from './grammar.js';
|
|
9
|
+
import { preview } from './mutations.js';
|
|
10
|
+
import { Redactor } from './redaction.js';
|
|
11
|
+
import { readCluster } from './runner.js';
|
|
12
|
+
import { readWorkspaceTar, relativeFile, workspaceTar } from './workspace-tar.js';
|
|
13
|
+
const valid = new Ajv().compile({ type: 'object', additionalProperties: false, required: ['action'], properties: {
|
|
14
|
+
action: { enum: ['start', 'confirm'] },
|
|
15
|
+
args: { type: 'array', maxItems: 256, items: { type: 'string', maxLength: 8192 } },
|
|
16
|
+
digest: { type: 'string', pattern: '^[a-f0-9]{64}$' },
|
|
17
|
+
stdinBase64: { type: 'string', maxLength: 4 * Math.ceil(100 * 1024 * 1024 / 3), pattern: '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$' },
|
|
18
|
+
inputFile: { const: 'stdin.bin' },
|
|
19
|
+
timeoutMs: { type: 'integer', minimum: 1, maximum: 1800000 },
|
|
20
|
+
} });
|
|
21
|
+
// A host-owned IPC worker keeps preview state and injected identity in one
|
|
22
|
+
// private call directory. It has no network listener or ambient identity route.
|
|
23
|
+
if (!process.send)
|
|
24
|
+
throw new Failure('PRIVATE_CHANNEL_REQUIRED', 'Launch bounded sessions through the authenticated host.');
|
|
25
|
+
const send = async (message) => new Promise((resolve, reject) => {
|
|
26
|
+
if (!process.connected) {
|
|
27
|
+
reject(new Failure('CANCELLED', 'The host disconnected.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
process.send(message, undefined, undefined, (error) => error ? reject(error) : resolve());
|
|
31
|
+
});
|
|
32
|
+
const redactor = new Redactor();
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
let busy = false;
|
|
35
|
+
let started = false;
|
|
36
|
+
let request;
|
|
37
|
+
let expectedDigest;
|
|
38
|
+
let input = Buffer.alloc(0);
|
|
39
|
+
let timeoutMs = 300000;
|
|
40
|
+
let selection;
|
|
41
|
+
let copying;
|
|
42
|
+
let downloadPath = '';
|
|
43
|
+
let remoteName = '';
|
|
44
|
+
let forwarding = false;
|
|
45
|
+
let forwardingOutput = '';
|
|
46
|
+
const forwards = new Map();
|
|
47
|
+
const artifact = [];
|
|
48
|
+
let artifactBytes = 0;
|
|
49
|
+
const run = async (invocation) => {
|
|
50
|
+
await send({ event: 'running' });
|
|
51
|
+
const output = await readCluster(selection, invocation, redactor, { input, signal: controller.signal, timeoutMs,
|
|
52
|
+
...(forwarding ? { controlStdout: true } : {}),
|
|
53
|
+
output: async (channel, bytes) => {
|
|
54
|
+
if (forwarding && channel === 'stdout') {
|
|
55
|
+
forwardingOutput += bytes.toString('utf8');
|
|
56
|
+
if (Buffer.byteLength(forwardingOutput) > 65536)
|
|
57
|
+
throw new Failure('OUTPUT_LIMIT', 'Port forwarding diagnostics exceeded their bound.');
|
|
58
|
+
let newline;
|
|
59
|
+
while ((newline = forwardingOutput.indexOf('\n')) >= 0) {
|
|
60
|
+
const line = forwardingOutput.slice(0, newline);
|
|
61
|
+
forwardingOutput = forwardingOutput.slice(newline + 1);
|
|
62
|
+
const match = /^Forwarding from 127\.0\.0\.1:([0-9]+) -> ([0-9]+)$/.exec(line);
|
|
63
|
+
if (match) {
|
|
64
|
+
forwards.set(Number(match[2]), Number(match[1]));
|
|
65
|
+
await send({ event: 'ports', ports: [...forwards].map(([remote, local]) => ({ remote, local })) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (copying === 'download' && channel === 'stdout') {
|
|
71
|
+
artifactBytes += bytes.length;
|
|
72
|
+
if (artifactBytes > 250 * 1024 * 1024 + 102400)
|
|
73
|
+
throw new Failure('TRANSFER_LIMIT', 'The artifact exceeds the workspace byte limit.');
|
|
74
|
+
artifact.push(bytes);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
await send({ event: 'output', channel, base64: bytes.toString('base64') });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const native = output;
|
|
81
|
+
if (copying === 'download' && native.exitCode === 0) {
|
|
82
|
+
const files = readWorkspaceTar(Buffer.concat(artifact));
|
|
83
|
+
const manifest = [];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
if (file.path !== remoteName && !file.path.startsWith(remoteName + '/'))
|
|
86
|
+
throw new Failure('PATH_REFUSED', 'The archive does not match the selected container path.');
|
|
87
|
+
const name = downloadPath + file.path.slice(remoteName.length);
|
|
88
|
+
const path = join(process.env.TMPDIR, 'artifacts', name);
|
|
89
|
+
await mkdir(posix.dirname(path), { recursive: true, mode: 0o700 });
|
|
90
|
+
await writeFile(path, file.bytes, { mode: 0o600, flag: 'wx' });
|
|
91
|
+
manifest.push({ path: name, bytes: file.bytes.length, sha256: createHash('sha256').update(file.bytes).digest('hex') });
|
|
92
|
+
}
|
|
93
|
+
await send({ event: 'artifacts', files: manifest });
|
|
94
|
+
}
|
|
95
|
+
if (!native.native)
|
|
96
|
+
await send({ event: 'result', value: redactor.value(output) });
|
|
97
|
+
await send({ event: 'exit', exitCode: native.exitCode ?? Number(process.exitCode ?? 0), stdoutEOF: true, stderrEOF: true });
|
|
98
|
+
process.disconnect();
|
|
99
|
+
};
|
|
100
|
+
process.on('disconnect', () => { controller.abort(); });
|
|
101
|
+
process.on('SIGTERM', () => { controller.abort(); });
|
|
102
|
+
process.on('SIGINT', () => { controller.abort(); });
|
|
103
|
+
process.on('message', (raw) => {
|
|
104
|
+
void (async () => {
|
|
105
|
+
if (!valid(raw))
|
|
106
|
+
throw new Failure('USAGE', 'The session message is invalid.', 2);
|
|
107
|
+
if (busy)
|
|
108
|
+
throw new Failure('USAGE', 'Wait for the current session operation.', 2);
|
|
109
|
+
busy = true;
|
|
110
|
+
if (raw.action === 'start') {
|
|
111
|
+
if (started || !raw.args)
|
|
112
|
+
throw new Failure('USAGE', 'Start a session once with ordered arguments.', 2);
|
|
113
|
+
started = true;
|
|
114
|
+
validateEnvironment();
|
|
115
|
+
timeoutMs = raw.timeoutMs ?? timeoutMs;
|
|
116
|
+
input = Buffer.from(raw.stdinBase64 ?? '', 'base64');
|
|
117
|
+
if (raw.inputFile) {
|
|
118
|
+
if (raw.stdinBase64 !== undefined)
|
|
119
|
+
throw new Failure('USAGE', 'Select one session input channel.', 2);
|
|
120
|
+
const file = await open(join(process.env.TMPDIR, raw.inputFile), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
121
|
+
try {
|
|
122
|
+
const info = await file.stat();
|
|
123
|
+
if (!info.isFile() || info.size > 100 * 1024 * 1024)
|
|
124
|
+
throw new Failure('TRANSFER_LIMIT', 'The input artifact exceeds the regular-file limit.');
|
|
125
|
+
input = await file.readFile();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await file.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let args = raw.args;
|
|
132
|
+
if (args[0] === 'prepare' && args[1] === '--' && args[2] === 'cp') {
|
|
133
|
+
const cp = args.slice(3);
|
|
134
|
+
if (cp.length !== 2)
|
|
135
|
+
throw new Failure('USAGE', 'Use cp <workspace-path> <pod:/absolute-path> or cp <pod:/absolute-path> <workspace-path>.', 2);
|
|
136
|
+
const from = cp[0];
|
|
137
|
+
const to = cp[1];
|
|
138
|
+
const remote = /^(?:([a-z0-9][a-z0-9.-]*)\/)?([a-z0-9][a-z0-9.-]*):\/(.+)$/;
|
|
139
|
+
const source = remote.exec(from);
|
|
140
|
+
const destination = remote.exec(to);
|
|
141
|
+
if (Boolean(source) === Boolean(destination))
|
|
142
|
+
throw new Failure('USAGE', 'Copy between one workspace path and one named container path.', 2);
|
|
143
|
+
const selected = source ?? destination;
|
|
144
|
+
const path = relativeFile(selected[3]);
|
|
145
|
+
copying = source ? 'download' : 'upload';
|
|
146
|
+
const workspace = join(process.env.TMPDIR, 'workspace');
|
|
147
|
+
const local = source && to.startsWith('artifact:') ? to.slice('artifact:'.length) : relative(workspace, source ? to : from).split(posix.sep).join('/');
|
|
148
|
+
relativeFile(local);
|
|
149
|
+
remoteName = posix.basename(path);
|
|
150
|
+
downloadPath = local;
|
|
151
|
+
if (copying === 'upload')
|
|
152
|
+
input = await workspaceTar(workspace, local, remoteName);
|
|
153
|
+
else if (input.length)
|
|
154
|
+
throw new Failure('USAGE', 'A copy download accepts no stdin.', 2);
|
|
155
|
+
args = ['prepare', '--', 'exec', selected[2], ...(selected[1] ? ['--namespace', selected[1]] : []), ...(copying === 'upload' ? ['-i'] : []), '--', 'tar', copying === 'upload' ? 'xf' : 'cf', '-', '-C', '/' + posix.dirname(path), ...(copying === 'download' ? ['--', posix.basename(path)] : [])];
|
|
156
|
+
}
|
|
157
|
+
const invocation = parse(args, true);
|
|
158
|
+
forwarding = invocation.command.path === 'port-forward';
|
|
159
|
+
if (input.length)
|
|
160
|
+
invocation.inputSha256 = createHash('sha256').update(input).digest('hex');
|
|
161
|
+
if (invocation.nativeConfig || invocation.trustExec || invocation.config || invocation.profile || invocation.help)
|
|
162
|
+
throw new Failure('USAGE', 'Sessions use only the host-selected profile.', 2);
|
|
163
|
+
if (!['exec', 'attach', 'logs', 'get', 'port-forward'].includes(invocation.command.path) && invocation.command.effect !== 'clusterWrite')
|
|
164
|
+
throw new Failure('CAPABILITY_UNAVAILABLE', 'This command is outside the bounded session contract.');
|
|
165
|
+
selection = await identity();
|
|
166
|
+
redactor.register(selection.secret, selection.profile.clientCertificate);
|
|
167
|
+
if (invocation.workflow?.kind === 'prepare') {
|
|
168
|
+
const prepared = await readCluster(selection, invocation, redactor, { input, signal: controller.signal,
|
|
169
|
+
timeoutMs: 15000, output: async () => { },
|
|
170
|
+
});
|
|
171
|
+
request = prepared.request;
|
|
172
|
+
const displayed = await preview(parse(['preview', request]));
|
|
173
|
+
expectedDigest = displayed.digest;
|
|
174
|
+
await send({ event: 'prepared', preview: redactor.value(displayed) });
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (invocation.workflow)
|
|
178
|
+
throw new Failure('USAGE', 'Start with prepare or a bounded read.', 2);
|
|
179
|
+
invocation.native = true;
|
|
180
|
+
await run(invocation);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
if (!request || !raw.digest || raw.digest !== expectedDigest)
|
|
185
|
+
throw new Failure('PREVIEW_MISMATCH', 'Confirm the exact session preview digest.');
|
|
186
|
+
const invocation = parse(['confirm', request, '--digest', raw.digest]);
|
|
187
|
+
request = undefined;
|
|
188
|
+
await run(invocation);
|
|
189
|
+
}
|
|
190
|
+
busy = false;
|
|
191
|
+
})().catch(async (error) => {
|
|
192
|
+
controller.abort();
|
|
193
|
+
const failure = error instanceof Failure ? error : new Failure('UPSTREAM_FAILED', 'The bounded session failed.');
|
|
194
|
+
if (process.connected) {
|
|
195
|
+
await send({ event: 'failed', code: failure.code, message: failure.message }).catch(() => { });
|
|
196
|
+
if (process.connected)
|
|
197
|
+
process.disconnect();
|
|
198
|
+
}
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const metadataSha256 = "b7ff45e522f0aa9b6d16b861cfcbf9414786b1f999bede2dcb1f628949c4fc49";
|
|
2
|
+
export declare const executionLimit = "In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects. Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts. Bounded noninteractive execution and existing hosted bounded sessions remain supported. 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.";
|
|
3
|
+
export declare const guidance = "Read Kubernetes resources using isolated credentials and pinned kubectl v1.32.2.\nUse --profile <name> to select an AXI identity.\nUse native -- <kubectl arguments> for native output; native --profile is upstream profiling and is blocked because it writes files.\nReads are bounded; inspect complete, truncated and continue before treating output as complete.\nWrites default off. Explicitly enable KUBERNETES_AXI_READ_ONLY=false, then use prepare -- <mutation>, preview <request-key>, and confirm <request-key> --digest <preview-digest>.\nPreparation reads exact targets; local preview makes no API calls. Confirmation is one-shot and refuses stale or changed targets. Bulk deletion also requires KUBERNETES_AXI_COLLECTION_DELETE=true.\nUse server-dry-run -- <mutation> for explicit API validation without persistence.\nExec/attach and named debug mutations require preparation and confirmation. In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects. Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts. Bounded noninteractive execution and existing hosted bounded sessions remain supported. 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. Native Windows support remains pending. Token issuance additionally requires KUBERNETES_AXI_ACQUIRE_CREDENTIAL=true, a duration of at most one hour, and --credential-fd <inherited-private-pipe> before confirm. Issued tokens are delivered once and never retained. Hosted workspace transfer, byte sessions, followed logs, watches and multi-port forwarding use caller-owned authenticated Artium sessions. 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. Their execution inside the CLI is refused before effects. Generic proxy listeners and credential overrides remain refused. Native completion, options, plugin list and version --client=true work without credentials.\nIn 0.1.x, run diff and its external helpers outside the CLI. Submit resulting changes through prepare, preview and confirm.\nRun kubernetes-axi --help for available commands and kubernetes-axi native -- <command> --help for generated upstream flags.\n";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Generated by scripts/generate-metadata.ts.
|
|
2
|
+
export const metadataSha256 = 'b7ff45e522f0aa9b6d16b861cfcbf9414786b1f999bede2dcb1f628949c4fc49';
|
|
3
|
+
export const executionLimit = "In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects. Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts. Bounded noninteractive execution and existing hosted bounded sessions remain supported. 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.";
|
|
4
|
+
export const guidance = "Read Kubernetes resources using isolated credentials and pinned kubectl v1.32.2.\nUse --profile <name> to select an AXI identity.\nUse native -- <kubectl arguments> for native output; native --profile is upstream profiling and is blocked because it writes files.\nReads are bounded; inspect complete, truncated and continue before treating output as complete.\nWrites default off. Explicitly enable KUBERNETES_AXI_READ_ONLY=false, then use prepare -- <mutation>, preview <request-key>, and confirm <request-key> --digest <preview-digest>.\nPreparation reads exact targets; local preview makes no API calls. Confirmation is one-shot and refuses stale or changed targets. Bulk deletion also requires KUBERNETES_AXI_COLLECTION_DELETE=true.\nUse server-dry-run -- <mutation> for explicit API validation without persistence.\nExec/attach and named debug mutations require preparation and confirmation. In 0.1.x, managed interactive exec/attach/debug and standalone watch, follow and port-forward requests are refused before effects. Credentials come only from existing config/profile and secure credential references; tool-managed sessions never read credential secrets from terminal prompts. Bounded noninteractive execution and existing hosted bounded sessions remain supported. 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. Native Windows support remains pending. Token issuance additionally requires KUBERNETES_AXI_ACQUIRE_CREDENTIAL=true, a duration of at most one hour, and --credential-fd <inherited-private-pipe> before confirm. Issued tokens are delivered once and never retained. Hosted workspace transfer, byte sessions, followed logs, watches and multi-port forwarding use caller-owned authenticated Artium sessions. 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. Their execution inside the CLI is refused before effects. Generic proxy listeners and credential overrides remain refused. Native completion, options, plugin list and version --client=true work without credentials.\nIn 0.1.x, run diff and its external helpers outside the CLI. Submit resulting changes through prepare, preview and confirm.\nRun kubernetes-axi --help for available commands and kubernetes-axi native -- <command> --help for generated upstream flags.\n";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Invocation } from './grammar.js';
|
|
2
|
+
import type { Redactor } from './redaction.js';
|
|
3
|
+
export declare function preview(invocation: Invocation): Promise<object>;
|
|
4
|
+
export declare function mutationSession(configFile: string, invocation: Invocation, namespace: string, redactor: Redactor, signal: AbortSignal, compatibleVersion: (value: unknown) => void, nativeBinding?: string): Promise<{
|
|
5
|
+
config: object;
|
|
6
|
+
argv?: string[];
|
|
7
|
+
rawPath?: string;
|
|
8
|
+
prepared?: boolean;
|
|
9
|
+
finish: (success?: boolean) => Promise<object>;
|
|
10
|
+
close: () => Promise<void>;
|
|
11
|
+
}>;
|