ag-ai 0.1.1 → 0.1.3

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.
@@ -24,6 +24,11 @@ export interface SecretMount {
24
24
  file: string;
25
25
  }
26
26
  export declare const secretMount: (name: string, reference: SecretReference) => SecretMount;
27
+ /**
28
+ * Mount one Secret key as one concrete file path, for tools that read a config
29
+ * file rather than an environment variable (for example OpenCode's auth.json).
30
+ */
31
+ export declare const secretFileMount: (name: string, reference: SecretReference, file: string) => SecretMount;
27
32
  export declare const env: (name: string, value: string) => V1EnvVar;
28
33
  /** OpenCode health probe with basic auth, evaluated inside the container. */
29
34
  export declare const openCodeHealthProbe: (port: number) => {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.openCodeHealthProbe = exports.env = exports.secretMount = exports.stripClusterScopedNamespaces = exports.serviceUrl = exports.resourceName = exports.METAL_LB_KEY = exports.T3_SERVICE = exports.RUNNER_SERVICE = exports.OPENCODE_SERVICE = exports.T3_PORT = exports.GENERATION_PORT = exports.OPENCODE_PORT = exports.T3CODE_APP = exports.RUNNER_APP = exports.OPENCODE_APP = exports.NAMESPACE = void 0;
3
+ exports.openCodeHealthProbe = exports.env = exports.secretFileMount = exports.secretMount = exports.stripClusterScopedNamespaces = exports.serviceUrl = exports.resourceName = exports.METAL_LB_KEY = exports.T3_SERVICE = exports.RUNNER_SERVICE = exports.OPENCODE_SERVICE = exports.T3_PORT = exports.GENERATION_PORT = exports.OPENCODE_PORT = exports.T3CODE_APP = exports.RUNNER_APP = exports.OPENCODE_APP = exports.NAMESPACE = void 0;
4
4
  const ag_rpi_mega_1 = require("ag-rpi-mega");
5
5
  /** The plugin owns its identity; addresses, hosts, and secrets come from the host. */
6
6
  exports.NAMESPACE = 'ag-ai';
@@ -39,6 +39,28 @@ const secretMount = (name, reference) => {
39
39
  return { volume, mount, file: `${mountPath}/${reference.key}` };
40
40
  };
41
41
  exports.secretMount = secretMount;
42
+ /**
43
+ * Mount one Secret key as one concrete file path, for tools that read a config
44
+ * file rather than an environment variable (for example OpenCode's auth.json).
45
+ */
46
+ const secretFileMount = (name, reference, file) => {
47
+ const volume = {
48
+ name: `${name}-file`,
49
+ secret: {
50
+ defaultMode: 0o400,
51
+ items: [{ key: reference.key, path: reference.key }],
52
+ secretName: reference.name,
53
+ },
54
+ };
55
+ const mount = {
56
+ mountPath: file,
57
+ name: volume.name,
58
+ readOnly: true,
59
+ subPath: reference.key,
60
+ };
61
+ return { volume, mount, file };
62
+ };
63
+ exports.secretFileMount = secretFileMount;
42
64
  const env = (name, value) => ({ name, value });
43
65
  exports.env = env;
44
66
  /** OpenCode health probe with basic auth, evaluated inside the container. */
package/dist/k8s/index.js CHANGED
@@ -20,6 +20,7 @@ exports.plugin = {
20
20
  'generation-token',
21
21
  'analytica-token',
22
22
  'opencode-password',
23
+ 'opencode-auth',
23
24
  'image-api-key',
24
25
  ],
25
26
  requiredImages: ['ag-ai-runner', 'ag-ai-opencode', 'ag-ai-t3code'],
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.opencodeStack = void 0;
4
4
  const ag_rpi_mega_1 = require("ag-rpi-mega");
5
5
  const common_1 = require("./common");
6
+ /** The published OpenCode CLI is installed into a writable emptyDir at pod start. */
7
+ const OPENCODE_PACKAGE = 'opencode-ai@1.18.31';
8
+ const OPENCODE_PREFIX = '/opt/opencode';
9
+ const OPENCODE_BIN = `${OPENCODE_PREFIX}/node_modules/.bin/opencode`;
6
10
  /** OpenCode server: durable state, basic auth from a mounted Secret, ClusterIP only. */
7
11
  const opencodeStack = (context) => {
8
12
  const { facilities } = context;
@@ -17,12 +21,14 @@ const opencodeStack = (context) => {
17
21
  (0, common_1.stripClusterScopedNamespaces)(stack);
18
22
  const password = context.secret('opencode-password');
19
23
  const credentials = (0, common_1.secretMount)('opencode-password', password);
24
+ // Provider credentials live in OpenCode's auth.json under XDG_DATA_HOME.
25
+ const auth = (0, common_1.secretFileMount)('opencode-auth', context.secret('opencode-auth'), '/data/opencode/data/opencode/auth.json');
20
26
  const probe = (0, common_1.openCodeHealthProbe)(common_1.OPENCODE_PORT);
21
27
  const container = {
22
28
  name: 'opencode',
23
29
  image: facilities.image('ag-ai-opencode'),
24
30
  imagePullPolicy: 'IfNotPresent',
25
- command: ['opencode', 'serve', '--hostname', '0.0.0.0', '--port', String(common_1.OPENCODE_PORT)],
31
+ command: [OPENCODE_BIN, 'serve', '--hostname', '0.0.0.0', '--port', String(common_1.OPENCODE_PORT)],
26
32
  ports: [{ name: 'opencode', containerPort: common_1.OPENCODE_PORT, protocol: 'TCP' }],
27
33
  env: [
28
34
  (0, ag_rpi_mega_1.secretEnv)('OPENCODE_SERVER_PASSWORD', password),
@@ -38,17 +44,58 @@ const opencodeStack = (context) => {
38
44
  requests: { cpu: '50m', memory: '256Mi' },
39
45
  limits: { cpu: '1000m', memory: '1Gi' },
40
46
  },
41
- volumeMounts: [...dataMounts, credentials.mount, { name: 'tmp', mountPath: '/tmp' }],
47
+ volumeMounts: [
48
+ ...dataMounts,
49
+ credentials.mount,
50
+ auth.mount,
51
+ { name: 'opencode', mountPath: OPENCODE_PREFIX, readOnly: true },
52
+ { name: 'tmp', mountPath: '/tmp' },
53
+ ],
42
54
  };
43
- stack.addDeployment({
55
+ // The postinstall script selects and copies the platform binary, so this
56
+ // install must not be run with --ignore-scripts.
57
+ const installer = {
58
+ name: 'install-opencode',
59
+ image: facilities.image('ag-ai-opencode'),
60
+ command: ['npm'],
61
+ args: ['install', '--prefix', OPENCODE_PREFIX, '--no-audit', '--no-fund', OPENCODE_PACKAGE],
62
+ env: [(0, common_1.env)('HOME', '/tmp'), (0, common_1.env)('NPM_CONFIG_CACHE', '/tmp/npm-cache')],
63
+ volumeMounts: [
64
+ { name: 'opencode', mountPath: OPENCODE_PREFIX },
65
+ { name: 'tmp', mountPath: '/tmp' },
66
+ ],
67
+ resources: {
68
+ requests: { cpu: '100m', memory: '256Mi' },
69
+ limits: { cpu: '1000m', memory: '1Gi' },
70
+ },
71
+ };
72
+ const deployment = stack.addDeployment({
44
73
  name: 'server',
45
74
  containers: [container],
46
- volumes: [dataVolume, credentials.volume, { name: 'tmp', emptyDir: { sizeLimit: '256Mi' } }],
75
+ initContainers: [installer],
76
+ volumes: [
77
+ dataVolume,
78
+ credentials.volume,
79
+ auth.volume,
80
+ { name: 'opencode', emptyDir: { sizeLimit: '1Gi' } },
81
+ { name: 'tmp', emptyDir: { sizeLimit: '1Gi' } },
82
+ ],
47
83
  replicas: 1,
48
84
  architecture: facilities.arch,
49
85
  securityMode: 'None',
50
86
  changeNFSFilePerms: true,
51
87
  });
88
+ const spec = deployment.spec;
89
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- the SDK class type hides runtime optionality.
90
+ const pod = spec?.template?.spec;
91
+ if (!spec || !pod)
92
+ throw new Error('opencode deployment is missing its pod specification');
93
+ // Group-own the emptyDirs for the non-root install and runtime containers.
94
+ pod.securityContext = {
95
+ fsGroup: 65534,
96
+ fsGroupChangePolicy: 'OnRootMismatch',
97
+ seccompProfile: { type: 'RuntimeDefault' },
98
+ };
52
99
  stack.addService({ name: common_1.OPENCODE_SERVICE, tcp: [common_1.OPENCODE_PORT], type: 'ClusterIP' });
53
100
  return stack;
54
101
  };
@@ -9,6 +9,10 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const ag_rpi_mega_1 = require("ag-rpi-mega");
10
10
  const common_1 = require("./common");
11
11
  const networkInitTemplate = (0, node_fs_1.readFileSync)(node_path_1.default.join(__dirname, 'assets', 'network-init.sh'), 'utf8');
12
+ /** The published runtime is installed into a writable emptyDir at pod start. */
13
+ const AG_AI_PACKAGE = 'ag-ai@0.1.1';
14
+ const AG_AI_RUNTIME_DIR = '/opt/ag-ai';
15
+ const AG_AI_BRIDGE = `${AG_AI_RUNTIME_DIR}/node_modules/ag-ai/runtime/bridge.mjs`;
12
16
  const networkInitScript = ({ generationCidr, generationPort, opencodePort, }) => networkInitTemplate
13
17
  .replaceAll('__GENERATION_CIDR__', generationCidr)
14
18
  .replaceAll('__GENERATION_PORT__', String(generationPort))
@@ -74,8 +78,8 @@ const runnerStack = (context) => {
74
78
  name: 'runner',
75
79
  image: facilities.image('ag-ai-runner'),
76
80
  imagePullPolicy: 'IfNotPresent',
77
- command: ['node', 'runtime/bridge.mjs'],
78
- workingDir: '/app',
81
+ command: ['node', AG_AI_BRIDGE],
82
+ workingDir: '/data',
79
83
  ports: [{ name: 'generation', containerPort: common_1.GENERATION_PORT, protocol: 'TCP' }],
80
84
  startupProbe: { ...healthProbe(false), failureThreshold: 30 },
81
85
  livenessProbe: { ...healthProbe(false), failureThreshold: 3 },
@@ -106,6 +110,7 @@ const runnerStack = (context) => {
106
110
  volumeMounts: [
107
111
  ...dataMounts,
108
112
  { name: 'runner-tmp', mountPath: '/tmp' },
113
+ { name: 'runtime', mountPath: AG_AI_RUNTIME_DIR, readOnly: true },
109
114
  secrets.runner.mount,
110
115
  secrets.generation.mount,
111
116
  secrets.analytica.mount,
@@ -119,6 +124,7 @@ const runnerStack = (context) => {
119
124
  volumes: [
120
125
  dataVolume,
121
126
  scriptVolume,
127
+ { name: 'runtime', emptyDir: { sizeLimit: '512Mi' } },
122
128
  { name: 'runner-tmp', emptyDir: { sizeLimit: '128Mi' } },
123
129
  secrets.runner.volume,
124
130
  secrets.generation.volume,
@@ -158,6 +164,39 @@ const runnerStack = (context) => {
158
164
  limits: { cpu: '500m', memory: '128Mi' },
159
165
  },
160
166
  },
167
+ {
168
+ name: 'install-ag-ai',
169
+ image: facilities.image('ag-ai-runner'),
170
+ command: ['npm'],
171
+ args: [
172
+ 'install',
173
+ '--prefix',
174
+ AG_AI_RUNTIME_DIR,
175
+ '--no-audit',
176
+ '--no-fund',
177
+ '--omit=dev',
178
+ '--ignore-scripts',
179
+ AG_AI_PACKAGE,
180
+ ],
181
+ env: [(0, common_1.env)('HOME', '/tmp'), (0, common_1.env)('NPM_CONFIG_CACHE', '/tmp/npm-cache')],
182
+ volumeMounts: [
183
+ { name: 'runtime', mountPath: AG_AI_RUNTIME_DIR },
184
+ { name: 'runner-tmp', mountPath: '/tmp' },
185
+ ],
186
+ securityContext: {
187
+ runAsNonRoot: true,
188
+ runAsUser: 65534,
189
+ runAsGroup: 65534,
190
+ readOnlyRootFilesystem: true,
191
+ allowPrivilegeEscalation: false,
192
+ capabilities: { drop: ['ALL'] },
193
+ seccompProfile: { type: 'RuntimeDefault' },
194
+ },
195
+ resources: {
196
+ requests: { cpu: '50m', memory: '64Mi' },
197
+ limits: { cpu: '500m', memory: '512Mi' },
198
+ },
199
+ },
161
200
  {
162
201
  name: 'prepare-generation-store',
163
202
  image: facilities.image('ag-ai-runner'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ag-ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Provider-neutral AI client and cluster runtime for the rpi-mega cluster.",
5
5
  "license": "ISC",
6
6
  "author": "admin@gec.dev",