ag-ai 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 (55) hide show
  1. package/README.md +31 -0
  2. package/dist/client/adapters/cluster.d.ts +12 -0
  3. package/dist/client/adapters/cluster.js +567 -0
  4. package/dist/client/adapters/google.d.ts +12 -0
  5. package/dist/client/adapters/google.js +478 -0
  6. package/dist/client/client.d.ts +11 -0
  7. package/dist/client/client.js +192 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +18 -0
  10. package/dist/client/quota.d.ts +55 -0
  11. package/dist/client/quota.js +148 -0
  12. package/dist/client/types.d.ts +124 -0
  13. package/dist/client/types.js +2 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +18 -0
  16. package/dist/internal/apikey.d.ts +5 -0
  17. package/dist/internal/apikey.js +51 -0
  18. package/dist/internal/array.d.ts +2 -0
  19. package/dist/internal/array.js +6 -0
  20. package/dist/internal/log.d.ts +9 -0
  21. package/dist/internal/log.js +92 -0
  22. package/dist/internal/node-cache.d.ts +71 -0
  23. package/dist/internal/node-cache.js +104 -0
  24. package/dist/internal/retryOnError.d.ts +26 -0
  25. package/dist/internal/retryOnError.js +123 -0
  26. package/dist/internal/string/redact.d.ts +2 -0
  27. package/dist/internal/string/redact.js +94 -0
  28. package/dist/internal/string/truncate.d.ts +8 -0
  29. package/dist/internal/string/truncate.js +16 -0
  30. package/dist/k8s/assets/network-init.sh +52 -0
  31. package/dist/k8s/common.d.ts +35 -0
  32. package/dist/k8s/common.js +56 -0
  33. package/dist/k8s/index.d.ts +9 -0
  34. package/dist/k8s/index.js +33 -0
  35. package/dist/k8s/opencode.d.ts +4 -0
  36. package/dist/k8s/opencode.js +55 -0
  37. package/dist/k8s/runner.d.ts +4 -0
  38. package/dist/k8s/runner.js +247 -0
  39. package/dist/k8s/t3code.d.ts +4 -0
  40. package/dist/k8s/t3code.js +60 -0
  41. package/package.json +56 -0
  42. package/runtime/bridge.mjs +1093 -0
  43. package/runtime/executor/opencode.mjs +430 -0
  44. package/runtime/image/adapter.mjs +213 -0
  45. package/runtime/index.mjs +53 -0
  46. package/runtime/protocol.mjs +384 -0
  47. package/runtime/server.mjs +621 -0
  48. package/runtime/store.mjs +568 -0
  49. package/runtime/telemetry.mjs +137 -0
  50. package/runtime/tests/bridge.test.mjs +554 -0
  51. package/runtime/tests/image-adapter.test.mjs +148 -0
  52. package/runtime/tests/protocol.test.mjs +156 -0
  53. package/runtime/tests/server.test.mjs +231 -0
  54. package/runtime/tests/store.test.mjs +114 -0
  55. package/runtime/tests/telemetry.test.mjs +138 -0
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runnerStack = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const ag_rpi_mega_1 = require("ag-rpi-mega");
10
+ const common_1 = require("./common");
11
+ const networkInitTemplate = (0, node_fs_1.readFileSync)(node_path_1.default.join(__dirname, 'assets', 'network-init.sh'), 'utf8');
12
+ const networkInitScript = ({ generationCidr, generationPort, opencodePort, }) => networkInitTemplate
13
+ .replaceAll('__GENERATION_CIDR__', generationCidr)
14
+ .replaceAll('__GENERATION_PORT__', String(generationPort))
15
+ .replaceAll('__OPENCODE_PORT__', String(opencodePort))
16
+ .replaceAll('__DNS_RULES__', '-A OUTPUT -p udp --dport 53 -j ACCEPT\n-A OUTPUT -p tcp --dport 53 -j ACCEPT')
17
+ .replaceAll('__GENERATION_REJECT__', `-A OUTPUT -d ${generationCidr} -j REJECT`);
18
+ const healthProbe = (readiness) => ({
19
+ exec: {
20
+ command: [
21
+ 'node',
22
+ '-e',
23
+ `try { const h = JSON.parse(require('node:fs').readFileSync('/tmp/runner-health.json','utf8')); process.exit(Date.now() - h.checkedAt < 60000${readiness ? ' && h.ready === true' : ''} ? 0 : 1); } catch { process.exit(1); }`,
24
+ ],
25
+ },
26
+ timeoutSeconds: 5,
27
+ periodSeconds: 10,
28
+ });
29
+ /** Queue worker: OpenCode executor, NFS generation store, LAN-only generation listener. */
30
+ const runnerStack = (context) => {
31
+ const { facilities } = context;
32
+ const queueUrl = context.env.AG_AI_QUEUE_URL;
33
+ if (!queueUrl)
34
+ throw new Error('AG_AI_QUEUE_URL is required');
35
+ const generationCidr = context.env.AG_AI_GENERATION_CIDR;
36
+ if (!generationCidr)
37
+ throw new Error('AG_AI_GENERATION_CIDR is required');
38
+ const imageProvider = context.env.AG_AI_IMAGE_PROVIDER ?? 'openai';
39
+ const opencodeProvider = context.env.AG_AI_OPENCODE_PROVIDER;
40
+ const metalLbAddress = facilities.metalLb(common_1.METAL_LB_KEY);
41
+ if (!metalLbAddress)
42
+ throw new Error(`no MetalLB address is configured for ${common_1.METAL_LB_KEY}`);
43
+ const stack = new ag_rpi_mega_1.Stack({ namespace: common_1.NAMESPACE, appName: common_1.RUNNER_APP, facilities });
44
+ const [dataVolume, dataMounts] = stack.addNFSVolume({
45
+ namePrefix: 'data',
46
+ mountPath: '/data',
47
+ path: facilities.nfs.exportPath,
48
+ server: facilities.nfs.hostname,
49
+ accessMode: 'ReadWriteOnce',
50
+ });
51
+ (0, common_1.stripClusterScopedNamespaces)(stack);
52
+ const [scriptVolume, scriptMount, scriptConfigMap] = stack.addConfigMapVolumes({
53
+ name: 'startup',
54
+ baseMountDirectory: '/etc/ag-ai',
55
+ preserveData: true,
56
+ data: {
57
+ 'network-init.sh': networkInitScript({
58
+ generationCidr,
59
+ generationPort: common_1.GENERATION_PORT,
60
+ opencodePort: common_1.OPENCODE_PORT,
61
+ }),
62
+ },
63
+ });
64
+ scriptConfigMap.immutable = true;
65
+ const secrets = {
66
+ runner: (0, common_1.secretMount)('runner-token', context.secret('runner-token')),
67
+ generation: (0, common_1.secretMount)('generation-token', context.secret('generation-token')),
68
+ analytica: (0, common_1.secretMount)('analytica-token', context.secret('analytica-token')),
69
+ opencode: (0, common_1.secretMount)('opencode-password', context.secret('opencode-password')),
70
+ image: (0, common_1.secretMount)('image-api-key', context.secret('image-api-key')),
71
+ };
72
+ const opencodeUrl = (0, common_1.serviceUrl)(facilities, common_1.OPENCODE_APP, common_1.OPENCODE_SERVICE, common_1.OPENCODE_PORT);
73
+ const container = {
74
+ name: 'runner',
75
+ image: facilities.image('ag-ai-runner'),
76
+ imagePullPolicy: 'IfNotPresent',
77
+ command: ['node', 'runtime/bridge.mjs'],
78
+ workingDir: '/app',
79
+ ports: [{ name: 'generation', containerPort: common_1.GENERATION_PORT, protocol: 'TCP' }],
80
+ startupProbe: { ...healthProbe(false), failureThreshold: 30 },
81
+ livenessProbe: { ...healthProbe(false), failureThreshold: 3 },
82
+ readinessProbe: { ...healthProbe(true), failureThreshold: 2 },
83
+ env: [
84
+ (0, common_1.env)('AG_AI_QUEUE_URL', queueUrl),
85
+ (0, common_1.env)('AG_AI_RUNNER_TOKEN_FILE', secrets.runner.file),
86
+ (0, common_1.env)('AG_AI_ANALYTICA_TOKEN_FILE', secrets.analytica.file),
87
+ (0, common_1.env)('AG_AI_OPENCODE_URL', opencodeUrl),
88
+ (0, common_1.env)('AG_AI_OPENCODE_PASSWORD_FILE', secrets.opencode.file),
89
+ ...(opencodeProvider === undefined ? [] : [(0, common_1.env)('AG_AI_OPENCODE_PROVIDER', opencodeProvider)]),
90
+ (0, common_1.env)('AG_AI_IMAGE_PROVIDER', imageProvider),
91
+ (0, common_1.env)('AG_AI_IMAGE_API_KEY_FILE', secrets.image.file),
92
+ (0, common_1.env)('AG_AI_RUNNER_ID', common_1.RUNNER_APP),
93
+ (0, common_1.env)('AG_AI_WORKSPACE', '/data/workspace'),
94
+ (0, common_1.env)('AG_AI_MAIN_WORKSPACE', '/data/workspace'),
95
+ (0, common_1.env)('AG_AI_GENERATED_IMAGES', '/data/generated-images'),
96
+ (0, common_1.env)('AG_AI_MAIN_GENERATED_IMAGES', '/data/generated-images'),
97
+ { name: 'AG_AI_GENERATION_HOST', valueFrom: { fieldRef: { fieldPath: 'status.podIP' } } },
98
+ (0, common_1.env)('AG_AI_GENERATION_PORT', String(common_1.GENERATION_PORT)),
99
+ (0, common_1.env)('AG_AI_GENERATION_STORE', '/data/generation-jobs'),
100
+ (0, common_1.env)('AG_AI_GENERATION_TOKEN_FILE', secrets.generation.file),
101
+ ],
102
+ resources: {
103
+ requests: { cpu: '50m', memory: '64Mi' },
104
+ limits: { cpu: '500m', memory: '512Mi' },
105
+ },
106
+ volumeMounts: [
107
+ ...dataMounts,
108
+ { name: 'runner-tmp', mountPath: '/tmp' },
109
+ secrets.runner.mount,
110
+ secrets.generation.mount,
111
+ secrets.analytica.mount,
112
+ secrets.opencode.mount,
113
+ secrets.image.mount,
114
+ ],
115
+ };
116
+ const deployment = stack.addDeployment({
117
+ name: 'runner',
118
+ containers: [container],
119
+ volumes: [
120
+ dataVolume,
121
+ scriptVolume,
122
+ { name: 'runner-tmp', emptyDir: { sizeLimit: '128Mi' } },
123
+ secrets.runner.volume,
124
+ secrets.generation.volume,
125
+ secrets.analytica.volume,
126
+ secrets.opencode.volume,
127
+ secrets.image.volume,
128
+ ],
129
+ replicas: 1,
130
+ architecture: facilities.arch,
131
+ securityMode: 'None',
132
+ changeNFSFilePerms: false,
133
+ });
134
+ const spec = deployment.spec;
135
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- the SDK class type hides runtime optionality.
136
+ const pod = spec?.template?.spec;
137
+ if (!spec || !pod)
138
+ throw new Error('runner deployment is missing its pod specification');
139
+ // A shared store must have only one writer during rollouts.
140
+ spec.strategy = { type: 'Recreate' };
141
+ pod.initContainers = [
142
+ {
143
+ name: 'restrict-network',
144
+ image: facilities.image('dockerAlpine'),
145
+ command: ['/bin/sh', '/etc/ag-ai/network-init.sh'],
146
+ volumeMounts: [{ name: scriptMount.name, mountPath: scriptMount.mountPath, readOnly: true }],
147
+ securityContext: {
148
+ runAsNonRoot: false,
149
+ runAsUser: 0,
150
+ runAsGroup: 0,
151
+ privileged: false,
152
+ allowPrivilegeEscalation: false,
153
+ capabilities: { drop: ['ALL'], add: ['NET_ADMIN'] },
154
+ seccompProfile: { type: 'RuntimeDefault' },
155
+ },
156
+ resources: {
157
+ requests: { cpu: '50m', memory: '32Mi' },
158
+ limits: { cpu: '500m', memory: '128Mi' },
159
+ },
160
+ },
161
+ {
162
+ name: 'prepare-generation-store',
163
+ image: facilities.image('ag-ai-runner'),
164
+ command: ['node', '-e'],
165
+ args: [
166
+ "require('node:fs').mkdirSync('/data/generation-jobs', { recursive: true, mode: 0o700 })",
167
+ ],
168
+ volumeMounts: dataMounts,
169
+ securityContext: {
170
+ runAsNonRoot: true,
171
+ runAsUser: 65534,
172
+ runAsGroup: 65534,
173
+ readOnlyRootFilesystem: true,
174
+ allowPrivilegeEscalation: false,
175
+ capabilities: { drop: ['ALL'] },
176
+ seccompProfile: { type: 'RuntimeDefault' },
177
+ },
178
+ resources: {
179
+ requests: { cpu: '10m', memory: '32Mi' },
180
+ limits: { cpu: '100m', memory: '64Mi' },
181
+ },
182
+ },
183
+ ];
184
+ pod.hostNetwork = false;
185
+ pod.hostPID = false;
186
+ pod.hostIPC = false;
187
+ pod.dnsPolicy = 'ClusterFirst';
188
+ pod.automountServiceAccountToken = false;
189
+ pod.shareProcessNamespace = false;
190
+ pod.enableServiceLinks = false;
191
+ pod.terminationGracePeriodSeconds = 60;
192
+ pod.securityContext = {
193
+ runAsNonRoot: true,
194
+ runAsUser: 65534,
195
+ runAsGroup: 65534,
196
+ fsGroup: 65534,
197
+ fsGroupChangePolicy: 'OnRootMismatch',
198
+ seccompProfile: { type: 'RuntimeDefault' },
199
+ };
200
+ // Only the authenticated generation listener is reachable from the LAN.
201
+ // Never add an Ingress, hostname route, public IP, or raw service.
202
+ stack.resources.push({
203
+ apiVersion: 'v1',
204
+ kind: 'Service',
205
+ metadata: {
206
+ name: (0, common_1.resourceName)(common_1.RUNNER_APP, common_1.RUNNER_SERVICE),
207
+ namespace: common_1.NAMESPACE,
208
+ annotations: { [facilities.annotations.addressPool]: common_1.METAL_LB_KEY },
209
+ },
210
+ spec: {
211
+ type: 'LoadBalancer',
212
+ loadBalancerIP: metalLbAddress,
213
+ loadBalancerSourceRanges: [generationCidr],
214
+ externalTrafficPolicy: 'Local',
215
+ allocateLoadBalancerNodePorts: false,
216
+ selector: { app: common_1.RUNNER_APP },
217
+ ports: [
218
+ {
219
+ name: 'generation',
220
+ protocol: 'TCP',
221
+ port: common_1.GENERATION_PORT,
222
+ targetPort: common_1.GENERATION_PORT,
223
+ },
224
+ ],
225
+ },
226
+ });
227
+ stack.resources.push({
228
+ apiVersion: 'networking.k8s.io/v1',
229
+ kind: 'NetworkPolicy',
230
+ metadata: {
231
+ name: (0, common_1.resourceName)(common_1.RUNNER_APP, 'generation-lan-only'),
232
+ namespace: common_1.NAMESPACE,
233
+ },
234
+ spec: {
235
+ podSelector: { matchLabels: { app: common_1.RUNNER_APP } },
236
+ policyTypes: ['Ingress'],
237
+ ingress: [
238
+ {
239
+ from: [{ ipBlock: { cidr: generationCidr } }],
240
+ ports: [{ protocol: 'TCP', port: common_1.GENERATION_PORT }],
241
+ },
242
+ ],
243
+ },
244
+ });
245
+ return stack;
246
+ };
247
+ exports.runnerStack = runnerStack;
@@ -0,0 +1,4 @@
1
+ import { Stack } from 'ag-rpi-mega';
2
+ import type { ClusterPluginContext } from 'ag-rpi-mega';
3
+ /** T3 Code control surface: durable home, OpenCode provider, outbound T3 Connect only. */
4
+ export declare const t3codeStack: (context: ClusterPluginContext) => Stack;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.t3codeStack = void 0;
4
+ const ag_rpi_mega_1 = require("ag-rpi-mega");
5
+ const common_1 = require("./common");
6
+ /** T3 Code control surface: durable home, OpenCode provider, outbound T3 Connect only. */
7
+ const t3codeStack = (context) => {
8
+ const { facilities } = context;
9
+ const stack = new ag_rpi_mega_1.Stack({ namespace: common_1.NAMESPACE, appName: common_1.T3CODE_APP, facilities });
10
+ const [dataVolume, dataMounts] = stack.addNFSVolume({
11
+ namePrefix: 'home',
12
+ mountPath: '/data',
13
+ path: facilities.nfs.exportPath,
14
+ server: facilities.nfs.hostname,
15
+ accessMode: 'ReadWriteOnce',
16
+ });
17
+ (0, common_1.stripClusterScopedNamespaces)(stack);
18
+ const opencodePassword = context.secret('opencode-password');
19
+ const credentials = (0, common_1.secretMount)('opencode-password', opencodePassword);
20
+ const opencodeUrl = (0, common_1.serviceUrl)(facilities, common_1.OPENCODE_APP, common_1.OPENCODE_SERVICE, common_1.OPENCODE_PORT);
21
+ const container = {
22
+ name: 't3code',
23
+ image: facilities.image('ag-ai-t3code'),
24
+ imagePullPolicy: 'IfNotPresent',
25
+ command: ['t3', 'serve', '--host', '0.0.0.0'],
26
+ ports: [{ name: 't3', containerPort: common_1.T3_PORT, protocol: 'TCP' }],
27
+ env: [
28
+ (0, common_1.env)('T3CODE_HOME', '/data/t3code'),
29
+ (0, common_1.env)('OPENCODE_URL', opencodeUrl),
30
+ (0, ag_rpi_mega_1.secretEnv)('OPENCODE_SERVER_PASSWORD', opencodePassword),
31
+ // T3 Connect is outbound-only; the server dials the relay, never the reverse.
32
+ (0, common_1.env)('T3CODE_CONNECT', '1'),
33
+ ],
34
+ startupProbe: { tcpSocket: { port: common_1.T3_PORT }, failureThreshold: 30, periodSeconds: 10 },
35
+ readinessProbe: { tcpSocket: { port: common_1.T3_PORT }, initialDelaySeconds: 5, periodSeconds: 10 },
36
+ livenessProbe: {
37
+ tcpSocket: { port: common_1.T3_PORT },
38
+ initialDelaySeconds: 15,
39
+ periodSeconds: 30,
40
+ },
41
+ resources: {
42
+ requests: { cpu: '50m', memory: '256Mi' },
43
+ limits: { cpu: '1000m', memory: '1Gi' },
44
+ },
45
+ volumeMounts: [...dataMounts, credentials.mount, { name: 'tmp', mountPath: '/tmp' }],
46
+ };
47
+ stack.addDeployment({
48
+ name: 'server',
49
+ containers: [container],
50
+ volumes: [dataVolume, credentials.volume, { name: 'tmp', emptyDir: { sizeLimit: '256Mi' } }],
51
+ replicas: 1,
52
+ architecture: facilities.arch,
53
+ securityMode: 'None',
54
+ changeNFSFilePerms: true,
55
+ });
56
+ // Internal only; T3 Connect is the outbound path. Never add an Ingress.
57
+ stack.addService({ name: common_1.T3_SERVICE, tcp: [common_1.T3_PORT], type: 'ClusterIP' });
58
+ return stack;
59
+ };
60
+ exports.t3codeStack = t3codeStack;
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "ag-ai",
3
+ "version": "0.1.0",
4
+ "description": "Provider-neutral AI client and cluster runtime for the rpi-mega cluster.",
5
+ "license": "ISC",
6
+ "author": "admin@gec.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/andreigec/ag-ai.git"
10
+ },
11
+ "files": [
12
+ "dist/**/*",
13
+ "runtime/**/*",
14
+ "README.md"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": "./dist/index.js",
20
+ "./client": "./dist/index.js",
21
+ "./k8s": "./dist/k8s/index.js",
22
+ "./runtime": "./runtime/index.mjs"
23
+ },
24
+ "scripts": {
25
+ "preinstall": "npx only-allow pnpm",
26
+ "format": "e7-oxfmt",
27
+ "lint": "e7-oxlint",
28
+ "typecheck": "tsc --noEmit",
29
+ "build": "rimraf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json && node scripts/copy-k8s-assets.mjs",
30
+ "test": "pnpm run test:client && pnpm run test:runtime",
31
+ "test:client": "tsx --test \"src/tests/**/*.test.ts\"",
32
+ "test:runtime": "node --test \"runtime/tests/**/*.test.mjs\""
33
+ },
34
+ "dependencies": {
35
+ "@google/genai": "^2.19.0",
36
+ "node-cache": "^5.1.2"
37
+ },
38
+ "devDependencies": {
39
+ "@kubernetes/client-node": "2.0.0",
40
+ "@types/node": "^26.4.1",
41
+ "ag-rpi-mega": "file:../ag-rpi-mega",
42
+ "eslint-config-e7npm": "^0.1.68",
43
+ "rimraf": "^6.1.3",
44
+ "tsx": "^4.23.13",
45
+ "typescript": "7.0.2"
46
+ },
47
+ "peerDependencies": {
48
+ "ag-rpi-mega": ">=0.1.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "ag-rpi-mega": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "packageManager": "pnpm@11.24.0"
56
+ }