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,104 @@
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.TypedNodeCache = void 0;
7
+ const node_cache_1 = __importDefault(require("node-cache"));
8
+ class TypedNodeCache {
9
+ cache;
10
+ constructor(options = {}) {
11
+ this.cache = new node_cache_1.default({
12
+ stdTTL: 3600, // 1 hour default
13
+ checkperiod: 120, // Check for expired keys every 2 minutes default
14
+ ...options,
15
+ });
16
+ }
17
+ /**
18
+ * Normalizes the key to lowercase for case-insensitive operations
19
+ */
20
+ normalizeKey(key) {
21
+ return key.toLowerCase();
22
+ }
23
+ /**
24
+ * Get a value from the cache
25
+ */
26
+ get(key) {
27
+ return this.cache.get(this.normalizeKey(key));
28
+ }
29
+ /**
30
+ * Set a value in the cache
31
+ */
32
+ set(key, value, ttl) {
33
+ if (ttl !== undefined) {
34
+ return this.cache.set(this.normalizeKey(key), value, ttl);
35
+ }
36
+ return this.cache.set(this.normalizeKey(key), value);
37
+ }
38
+ /**
39
+ * Delete a key from the cache
40
+ */
41
+ del(key) {
42
+ return this.cache.del(this.normalizeKey(key));
43
+ }
44
+ /**
45
+ * Check if a key exists in the cache
46
+ */
47
+ has(key) {
48
+ return this.cache.has(this.normalizeKey(key));
49
+ }
50
+ /**
51
+ * Get multiple values from the cache
52
+ */
53
+ mget(keys) {
54
+ const normalizedKeys = keys.map((key) => this.normalizeKey(key));
55
+ return this.cache.mget(normalizedKeys);
56
+ }
57
+ /**
58
+ * Set multiple values in the cache
59
+ */
60
+ mset(keyValuePairs) {
61
+ const normalizedPairs = keyValuePairs.map((pair) => ({
62
+ ...pair,
63
+ key: this.normalizeKey(pair.key),
64
+ }));
65
+ return this.cache.mset(normalizedPairs);
66
+ }
67
+ /**
68
+ * Get all keys in the cache
69
+ */
70
+ keys() {
71
+ return this.cache.keys();
72
+ }
73
+ /**
74
+ * Get cache statistics
75
+ */
76
+ getStats() {
77
+ return this.cache.getStats();
78
+ }
79
+ /**
80
+ * Flush all data from the cache
81
+ */
82
+ flushAll() {
83
+ this.cache.flushAll();
84
+ }
85
+ /**
86
+ * Close the cache and clear all timers
87
+ */
88
+ close() {
89
+ this.cache.close();
90
+ }
91
+ /**
92
+ * Get the TTL for a key
93
+ */
94
+ getTtl(key) {
95
+ return this.cache.getTtl(this.normalizeKey(key));
96
+ }
97
+ /**
98
+ * Set the TTL for a key
99
+ */
100
+ ttl(key, ttl) {
101
+ return this.cache.ttl(this.normalizeKey(key), ttl);
102
+ }
103
+ }
104
+ exports.TypedNodeCache = TypedNodeCache;
@@ -0,0 +1,26 @@
1
+ export declare const overloadedMessages: string[];
2
+ export declare const retryableErrorMessages: string[];
3
+ export declare const isOverloadedApiKeyError: (error: unknown) => boolean;
4
+ export declare const isRetryableApiError: (error: unknown) => boolean;
5
+ export type RetryOnErrorOptions = {
6
+ /** Number of retries after the initial attempt. Defaults to 1. */
7
+ retries?: number;
8
+ /** Delay between attempts in ms. Defaults to 2000. */
9
+ errorDelay?: number;
10
+ /** Predicate deciding whether a failure is worth retrying. */
11
+ errorCheck?: (error: unknown) => boolean;
12
+ /** AbortSignal cancelling the backoff sleep and further attempts. */
13
+ signal?: AbortSignal;
14
+ /**
15
+ * Delay before each attempt after the first, in ms. It composes with
16
+ * errorDelay: the attempt waits for initialDelayMs plus errorDelay.
17
+ * Defaults to 0 (no pacing).
18
+ */
19
+ initialDelayMs?: number;
20
+ /** Sleep implementation, overridable in tests. */
21
+ sleepFn?: (ms: number) => Promise<unknown>;
22
+ };
23
+ export declare function retryOnError<T>(
24
+ /** so we can log retries with useful info */
25
+ debugIdent: string, fn: () => Promise<T>, retriesOrOptions?: number | RetryOnErrorOptions, errorDelay?: number, errorCheck?: (error: unknown) => boolean, signal?: AbortSignal): Promise<T>;
26
+ export declare const sleep: (ms: number) => Promise<unknown>;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sleep = exports.isRetryableApiError = exports.isOverloadedApiKeyError = exports.retryableErrorMessages = exports.overloadedMessages = void 0;
4
+ exports.retryOnError = retryOnError;
5
+ const log_1 = require("./log");
6
+ exports.overloadedMessages = [
7
+ '429',
8
+ 'current usage',
9
+ 'current quota',
10
+ 'overloaded',
11
+ 'quota exceeded',
12
+ 'quota failure',
13
+ 'rate limited',
14
+ 'too many requests',
15
+ 'generate_content_free_tier_requests',
16
+ 'generaterequestsperday',
17
+ 'high demand',
18
+ 'model pool exhausted',
19
+ 'no available model',
20
+ ];
21
+ exports.retryableErrorMessages = [
22
+ ...exports.overloadedMessages,
23
+ '500',
24
+ '503',
25
+ 'safety',
26
+ 'invalid_type',
27
+ 'expected',
28
+ ];
29
+ const errorText = (error) => {
30
+ if (typeof error === 'string') {
31
+ return error;
32
+ }
33
+ if (error instanceof Error) {
34
+ const nested = error.cause;
35
+ const nestedText = nested === undefined ? '' : ` ${errorText(nested)}`;
36
+ return `${error.message}${nestedText}`;
37
+ }
38
+ if (typeof error === 'object' && error !== null) {
39
+ try {
40
+ return JSON.stringify(error);
41
+ }
42
+ catch {
43
+ return Object.prototype.toString.call(error);
44
+ }
45
+ }
46
+ return String(error);
47
+ };
48
+ const errorContainsAnyMessage = (error, messages) => {
49
+ const message = errorText(error).toLowerCase();
50
+ return messages.some((m) => message.includes(m.toLowerCase()));
51
+ };
52
+ const isOverloadedApiKeyError = (error) => errorContainsAnyMessage(error, exports.overloadedMessages);
53
+ exports.isOverloadedApiKeyError = isOverloadedApiKeyError;
54
+ const isRetryableApiError = (error) => errorContainsAnyMessage(error, exports.retryableErrorMessages);
55
+ exports.isRetryableApiError = isRetryableApiError;
56
+ const createAbortError = () => {
57
+ const error = new Error('Operation aborted');
58
+ error.name = 'AbortError';
59
+ return error;
60
+ };
61
+ const awaitWithSignal = async (promise, signal) => {
62
+ if (signal === undefined)
63
+ return promise;
64
+ if (signal.aborted)
65
+ throw createAbortError();
66
+ const abort = new Promise((_, reject) => {
67
+ const onAbort = () => reject(createAbortError());
68
+ signal.addEventListener('abort', onAbort, { once: true });
69
+ void promise.then(() => signal.removeEventListener('abort', onAbort), () => signal.removeEventListener('abort', onAbort));
70
+ });
71
+ return Promise.race([promise, abort]);
72
+ };
73
+ const normalizeRetryOnErrorOptions = (retriesOrOptions, errorDelay, errorCheck, signal) => ({
74
+ retries: typeof retriesOrOptions === 'number' ? retriesOrOptions : (retriesOrOptions?.retries ?? 1),
75
+ errorDelay: typeof retriesOrOptions === 'number'
76
+ ? (errorDelay ?? 2000)
77
+ : (retriesOrOptions?.errorDelay ?? 2000),
78
+ errorCheck: typeof retriesOrOptions === 'number'
79
+ ? (errorCheck ?? exports.isRetryableApiError)
80
+ : (retriesOrOptions?.errorCheck ?? exports.isRetryableApiError),
81
+ signal: typeof retriesOrOptions === 'number' ? signal : (retriesOrOptions?.signal ?? signal),
82
+ initialDelayMs: typeof retriesOrOptions === 'number' ? 0 : Math.max(0, retriesOrOptions?.initialDelayMs ?? 0),
83
+ sleepFn: typeof retriesOrOptions === 'number' ? undefined : retriesOrOptions?.sleepFn,
84
+ });
85
+ async function retryOnError(
86
+ /** so we can log retries with useful info */
87
+ debugIdent, fn, retriesOrOptions, errorDelay, errorCheck = exports.isRetryableApiError, signal) {
88
+ const options = normalizeRetryOnErrorOptions(retriesOrOptions, errorDelay, errorCheck, signal);
89
+ const sleepWithSignal = async (ms) => {
90
+ if (ms <= 0)
91
+ return;
92
+ if (options.sleepFn !== undefined) {
93
+ await options.sleepFn(ms);
94
+ return;
95
+ }
96
+ await awaitWithSignal((0, exports.sleep)(ms), options.signal);
97
+ };
98
+ // Pacing delay before attempts after the first: it spreads bursty fan-out
99
+ // (e.g. nightly jobs hitting a low free-tier quota) without changing the
100
+ // retry semantics for direct callers.
101
+ await sleepWithSignal(options.initialDelayMs);
102
+ try {
103
+ const result = await fn();
104
+ return result;
105
+ }
106
+ catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ if (options.retries > 0 && options.errorCheck(error)) {
109
+ (0, log_1.info)(`Operation ${debugIdent} failed. Retrying after ${options.errorDelay}ms...`, message);
110
+ await sleepWithSignal(options.errorDelay);
111
+ return retryOnError(debugIdent, fn, {
112
+ retries: options.retries - 1,
113
+ errorDelay: options.errorDelay,
114
+ errorCheck: options.errorCheck,
115
+ signal: options.signal,
116
+ sleepFn: options.sleepFn,
117
+ });
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
123
+ exports.sleep = sleep;
@@ -0,0 +1,2 @@
1
+ export declare function redactString(str: string): string;
2
+ export declare function redactObject<T>(ob: T): T;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactString = redactString;
4
+ exports.redactObject = redactObject;
5
+ function redactString(str) {
6
+ let ret = str;
7
+ ret = ret || '';
8
+ const repl = '$1<redacted>$2';
9
+ // OAuth and authentication tokens
10
+ ret = ret.replace(/(\b)grant_type.+?(\b)/gm, repl);
11
+ ret = ret.replace(/(\b)Bearer .+?(\b)/gm, repl);
12
+ // JWT tokens (base64 with dots)
13
+ ret = ret.replace(/(eyJ[\w-_.]*\.[\w-_.]*\.[\w-_.]*)/gim, '<redacted>');
14
+ // API Keys (various formats)
15
+ ret = ret.replace(/(\b)(sk_live_|sk_test_|pk_live_|pk_test_)[\w-]+/gi, '$1<redacted>'); // Stripe
16
+ ret = ret.replace(/(\b)(AKIA[0-9A-Z]{16})/gi, '$1<redacted>'); // AWS Access Key ID
17
+ ret = ret.replace(/(aws_secret_access_key["\s]*[:=]["\s]*)([0-9a-zA-Z/+]{40})/gi, '$1<redacted>'); // AWS Secret Access Key pattern
18
+ ret = ret.replace(/(\b)(AIza[0-9A-Za-z\\-_]{35})/gi, '$1<redacted>'); // Google API Key
19
+ ret = ret.replace(/(\b)(ya29\.[0-9A-Za-z\\-_]+)/gi, '$1<redacted>'); // Google OAuth Access Token
20
+ ret = ret.replace(/(\b)(glpat-[a-zA-Z0-9\\-_]{20})/gi, '$1<redacted>'); // GitLab Personal Access Token
21
+ ret = ret.replace(/(\b)(ghp_[a-zA-Z0-9]{36})/gi, '$1<redacted>'); // GitHub Personal Access Token
22
+ ret = ret.replace(/(\b)(github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})/gi, '$1<redacted>'); // GitHub Fine-grained PAT
23
+ // Password patterns in key-value pairs
24
+ ret = ret.replace(/(password["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
25
+ ret = ret.replace(/(passwd["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
26
+ ret = ret.replace(/(pwd["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
27
+ ret = ret.replace(/(secret["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
28
+ ret = ret.replace(/(token["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
29
+ ret = ret.replace(/(^|[^a-zA-Z])(api_?key|private_?key|secret_?key)["\s]*[:=]["\s]*([^",\s}]+)/gi, '$1$2<redacted>');
30
+ ret = ret.replace(/(auth["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
31
+ // Database connection strings
32
+ ret = ret.replace(/(mongodb:\/\/[^:]+:)([^@]+)(@)/gi, '$1<redacted>$3');
33
+ ret = ret.replace(/(mysql:\/\/[^:]+:)([^@]+)(@)/gi, '$1<redacted>$3');
34
+ ret = ret.replace(/(postgresql:\/\/[^:]+:)([^@]+)(@)/gi, '$1<redacted>$3');
35
+ ret = ret.replace(/(redis:\/\/[^:]+:)([^@]+)(@)/gi, '$1<redacted>$3');
36
+ // Credit card numbers (basic pattern)
37
+ ret = ret.replace(/(\b)([3-6]\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{3,4})(\b)/g, '$1<redacted>$3');
38
+ // Social Security Numbers (US format)
39
+ ret = ret.replace(/(\b)(\d{3}[-\s]?\d{2}[-\s]?\d{4})(\b)/g, '$1<redacted>$3');
40
+ // Private keys
41
+ ret = ret.replace(/(-----BEGIN [A-Z\s]+PRIVATE KEY-----)([\s\S]*?)(-----END [A-Z\s]+PRIVATE KEY-----)/gi, '$1\n<redacted>\n$3');
42
+ // Session IDs and UUIDs in headers/cookies
43
+ ret = ret.replace(/(sessionid["\s]*[:=]["\s]*)([a-zA-Z0-9]{32,})/gi, '$1<redacted>');
44
+ ret = ret.replace(/(session["\s]*[:=]["\s]*)([a-zA-Z0-9]{32,})/gi, '$1<redacted>');
45
+ ret = ret.replace(/(csrftoken["\s]*[:=]["\s]*)([a-zA-Z0-9]{32,})/gi, '$1<redacted>');
46
+ // Common environment variable patterns
47
+ ret = ret.replace(/(DATABASE_URL["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
48
+ ret = ret.replace(/(REDIS_URL["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
49
+ ret = ret.replace(/(SMTP_PASSWORD["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
50
+ ret = ret.replace(/(ENCRYPTION_KEY["\s]*[:=]["\s]*)([^",\s}]+)/gi, '$1<redacted>');
51
+ // High entropy strings and long base64-like strings
52
+ ret = ret.replace(/\b([a-zA-Z0-9+/=_-]{20,})\b/g, (match) => {
53
+ // If it ends with ==, it's almost certainly a base64 string provided it is long enough
54
+ // We use a slightly higher length threshold for this specific check to be safe
55
+ if (match.endsWith('==') && match.length > 30) {
56
+ return '<redacted>';
57
+ }
58
+ // Check entropy for other long strings
59
+ if (getShannonEntropy(match) > 4.5) {
60
+ return '<redacted>';
61
+ }
62
+ return match;
63
+ });
64
+ return ret;
65
+ }
66
+ function getShannonEntropy(str) {
67
+ const len = str.length;
68
+ if (len === 0)
69
+ return 0;
70
+ const frequencies = new Map();
71
+ for (const char of str) {
72
+ frequencies.set(char, (frequencies.get(char) ?? 0) + 1);
73
+ }
74
+ let entropy = 0;
75
+ for (const count of frequencies.values()) {
76
+ const p = count / len;
77
+ entropy -= p * Math.log2(p);
78
+ }
79
+ return entropy;
80
+ }
81
+ function redactObject(ob) {
82
+ if (typeof ob === 'string') {
83
+ return redactString(ob);
84
+ }
85
+ else if (typeof ob === 'object') {
86
+ try {
87
+ return JSON.parse(redactString(JSON.stringify(ob)));
88
+ }
89
+ catch {
90
+ return ob;
91
+ }
92
+ }
93
+ return ob;
94
+ }
@@ -0,0 +1,8 @@
1
+ /** truncate text */
2
+ export declare function truncate(
3
+ /** string to truncate */
4
+ str: string,
5
+ /** max length */
6
+ n: number,
7
+ /** default '...' */
8
+ ellip?: string): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.truncate = truncate;
4
+ /** truncate text */
5
+ function truncate(
6
+ /** string to truncate */
7
+ str,
8
+ /** max length */
9
+ n,
10
+ /** default '...' */
11
+ ellip = '...') {
12
+ if (!str) {
13
+ return str;
14
+ }
15
+ return str.length > n ? str.slice(0, n - 1) + ellip : str;
16
+ }
@@ -0,0 +1,52 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ # Runs only in this pod's network namespace, before the runtime container.
5
+ # No host networking, host mounts, credentials, or privileged container required.
6
+ # Fail startup if either address family's rules cannot be installed.
7
+ apk add --no-cache --no-scripts iptables
8
+
9
+ iptables-restore <<'RULES'
10
+ *filter
11
+ :INPUT DROP [0:0]
12
+ :FORWARD DROP [0:0]
13
+ :OUTPUT DROP [0:0]
14
+ -A INPUT -i lo -j ACCEPT
15
+ -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
16
+ -A INPUT -s __GENERATION_CIDR__ -p tcp --dport __GENERATION_PORT__ -j ACCEPT
17
+ -A OUTPUT -o lo -j ACCEPT
18
+ -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
19
+ __DNS_RULES__
20
+ -A OUTPUT -p tcp --dport __OPENCODE_PORT__ -j ACCEPT
21
+ -A OUTPUT -d 0.0.0.0/8 -j REJECT
22
+ -A OUTPUT -d 10.0.0.0/8 -j REJECT
23
+ -A OUTPUT -d 100.64.0.0/10 -j REJECT
24
+ -A OUTPUT -d 127.0.0.0/8 -j REJECT
25
+ -A OUTPUT -d 169.254.0.0/16 -j REJECT
26
+ -A OUTPUT -d 172.16.0.0/12 -j REJECT
27
+ -A OUTPUT -d 192.0.0.0/24 -j REJECT
28
+ -A OUTPUT -d 192.0.2.0/24 -j REJECT
29
+ __GENERATION_REJECT__
30
+ -A OUTPUT -d 198.18.0.0/15 -j REJECT
31
+ -A OUTPUT -d 198.51.100.0/24 -j REJECT
32
+ -A OUTPUT -d 203.0.113.0/24 -j REJECT
33
+ -A OUTPUT -d 224.0.0.0/4 -j REJECT
34
+ -A OUTPUT -d 240.0.0.0/4 -j REJECT
35
+ -A OUTPUT -p tcp --dport 443 -j ACCEPT
36
+ COMMIT
37
+ RULES
38
+
39
+ # There is no IPv6 dependency; deny it entirely except pod loopback. This also
40
+ # closes IPv4-mapped, NAT64, link-local, and private IPv6 bypasses.
41
+ ip6tables-restore <<'RULES'
42
+ *filter
43
+ :INPUT DROP [0:0]
44
+ :FORWARD DROP [0:0]
45
+ :OUTPUT DROP [0:0]
46
+ -A INPUT -i lo -j ACCEPT
47
+ -A OUTPUT -o lo -j ACCEPT
48
+ -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
49
+ COMMIT
50
+ RULES
51
+
52
+ echo 'ag-ai pod firewall installed: authenticated LAN generation ingress; cluster DNS, OpenCode, and public IPv4 HTTPS egress'
@@ -0,0 +1,35 @@
1
+ import type { V1EnvVar, V1Volume, V1VolumeMount } from '@kubernetes/client-node';
2
+ import type { ClusterFacilities, IStack, SecretReference } from 'ag-rpi-mega';
3
+ /** The plugin owns its identity; addresses, hosts, and secrets come from the host. */
4
+ export declare const NAMESPACE = "ag-ai";
5
+ export declare const OPENCODE_APP = "ag-ai-opencode";
6
+ export declare const RUNNER_APP = "ag-ai-runner";
7
+ export declare const T3CODE_APP = "ag-ai-t3code";
8
+ export declare const OPENCODE_PORT = 4096;
9
+ export declare const GENERATION_PORT = 4501;
10
+ export declare const T3_PORT = 4001;
11
+ export declare const OPENCODE_SERVICE = "server";
12
+ export declare const RUNNER_SERVICE = "generation";
13
+ export declare const T3_SERVICE = "server";
14
+ export declare const METAL_LB_KEY = "ag-ai";
15
+ /** Mirrors the SDK's resource naming so service DNS is deterministic. */
16
+ export declare const resourceName: (appName: string, name: string) => string;
17
+ export declare const serviceUrl: (facilities: ClusterFacilities, appName: string, name: string, port: number) => string;
18
+ /** StorageClass and PersistentVolume are cluster-scoped; the SDK adds a namespace. */
19
+ export declare const stripClusterScopedNamespaces: (stack: IStack) => void;
20
+ export interface SecretMount {
21
+ volume: V1Volume;
22
+ mount: V1VolumeMount;
23
+ /** Absolute file path the mounted key resolves to inside the container. */
24
+ file: string;
25
+ }
26
+ export declare const secretMount: (name: string, reference: SecretReference) => SecretMount;
27
+ export declare const env: (name: string, value: string) => V1EnvVar;
28
+ /** OpenCode health probe with basic auth, evaluated inside the container. */
29
+ export declare const openCodeHealthProbe: (port: number) => {
30
+ exec: {
31
+ command: string[];
32
+ };
33
+ timeoutSeconds: number;
34
+ periodSeconds: number;
35
+ };
@@ -0,0 +1,56 @@
1
+ "use strict";
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;
4
+ const ag_rpi_mega_1 = require("ag-rpi-mega");
5
+ /** The plugin owns its identity; addresses, hosts, and secrets come from the host. */
6
+ exports.NAMESPACE = 'ag-ai';
7
+ exports.OPENCODE_APP = 'ag-ai-opencode';
8
+ exports.RUNNER_APP = 'ag-ai-runner';
9
+ exports.T3CODE_APP = 'ag-ai-t3code';
10
+ exports.OPENCODE_PORT = 4096;
11
+ exports.GENERATION_PORT = 4501;
12
+ exports.T3_PORT = 4001;
13
+ exports.OPENCODE_SERVICE = 'server';
14
+ exports.RUNNER_SERVICE = 'generation';
15
+ exports.T3_SERVICE = 'server';
16
+ exports.METAL_LB_KEY = 'ag-ai';
17
+ /** Mirrors the SDK's resource naming so service DNS is deterministic. */
18
+ const resourceName = (appName, name) => [appName, name].filter(Boolean).join('-');
19
+ exports.resourceName = resourceName;
20
+ const serviceUrl = (facilities, appName, name, port) => `http://${(0, exports.resourceName)(appName, name)}.${exports.NAMESPACE}.svc.${facilities.kubeDomain}:${port}`;
21
+ exports.serviceUrl = serviceUrl;
22
+ /** StorageClass and PersistentVolume are cluster-scoped; the SDK adds a namespace. */
23
+ const stripClusterScopedNamespaces = (stack) => {
24
+ for (const resource of stack.resources) {
25
+ if ((resource.kind === 'StorageClass' || resource.kind === 'PersistentVolume') &&
26
+ resource.metadata) {
27
+ delete resource.metadata.namespace;
28
+ }
29
+ }
30
+ };
31
+ exports.stripClusterScopedNamespaces = stripClusterScopedNamespaces;
32
+ const secretMount = (name, reference) => {
33
+ const mountPath = `/var/run/secrets/ag-ai/${name}`;
34
+ const [volume, mount] = (0, ag_rpi_mega_1.credentialVolume)({
35
+ name: `${name}-credentials`,
36
+ mountPath,
37
+ secrets: [reference],
38
+ });
39
+ return { volume, mount, file: `${mountPath}/${reference.key}` };
40
+ };
41
+ exports.secretMount = secretMount;
42
+ const env = (name, value) => ({ name, value });
43
+ exports.env = env;
44
+ /** OpenCode health probe with basic auth, evaluated inside the container. */
45
+ const openCodeHealthProbe = (port) => ({
46
+ exec: {
47
+ command: [
48
+ 'node',
49
+ '-e',
50
+ `fetch('http://127.0.0.1:${port}/global/health',{headers:{authorization:'Basic '+Buffer.from('opencode:'+(process.env.OPENCODE_SERVER_PASSWORD??'')).toString('base64')}}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))`,
51
+ ],
52
+ },
53
+ timeoutSeconds: 5,
54
+ periodSeconds: 10,
55
+ });
56
+ exports.openCodeHealthProbe = openCodeHealthProbe;
@@ -0,0 +1,9 @@
1
+ import type { ClusterPlugin } from 'ag-rpi-mega';
2
+ /**
3
+ * ag-ai cluster plugin. All cluster-owned values (images, MetalLB addresses, the
4
+ * NAS host, architecture, domain, secrets, and the LAN CIDR/queue URL) are
5
+ * resolved from the host through `ClusterPluginContext`; this package contains
6
+ * only logical names and generic container paths/ports.
7
+ */
8
+ export declare const plugin: ClusterPlugin;
9
+ export { NAMESPACE } from './common';
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NAMESPACE = exports.plugin = void 0;
4
+ const common_1 = require("./common");
5
+ const opencode_1 = require("./opencode");
6
+ const runner_1 = require("./runner");
7
+ const t3code_1 = require("./t3code");
8
+ /**
9
+ * ag-ai cluster plugin. All cluster-owned values (images, MetalLB addresses, the
10
+ * NAS host, architecture, domain, secrets, and the LAN CIDR/queue URL) are
11
+ * resolved from the host through `ClusterPluginContext`; this package contains
12
+ * only logical names and generic container paths/ports.
13
+ */
14
+ exports.plugin = {
15
+ name: 'ag-ai',
16
+ namespace: common_1.NAMESPACE,
17
+ requiredEnv: ['AG_AI_QUEUE_URL', 'AG_AI_GENERATION_CIDR'],
18
+ requiredSecrets: [
19
+ 'runner-token',
20
+ 'generation-token',
21
+ 'analytica-token',
22
+ 'opencode-password',
23
+ 'image-api-key',
24
+ ],
25
+ requiredImages: ['ag-ai-runner', 'ag-ai-opencode', 'ag-ai-t3code'],
26
+ stacks: (context) => [
27
+ (0, opencode_1.opencodeStack)(context),
28
+ (0, runner_1.runnerStack)(context),
29
+ (0, t3code_1.t3codeStack)(context),
30
+ ],
31
+ };
32
+ var common_2 = require("./common");
33
+ Object.defineProperty(exports, "NAMESPACE", { enumerable: true, get: function () { return common_2.NAMESPACE; } });
@@ -0,0 +1,4 @@
1
+ import { Stack } from 'ag-rpi-mega';
2
+ import type { ClusterPluginContext } from 'ag-rpi-mega';
3
+ /** OpenCode server: durable state, basic auth from a mounted Secret, ClusterIP only. */
4
+ export declare const opencodeStack: (context: ClusterPluginContext) => Stack;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.opencodeStack = void 0;
4
+ const ag_rpi_mega_1 = require("ag-rpi-mega");
5
+ const common_1 = require("./common");
6
+ /** OpenCode server: durable state, basic auth from a mounted Secret, ClusterIP only. */
7
+ const opencodeStack = (context) => {
8
+ const { facilities } = context;
9
+ const stack = new ag_rpi_mega_1.Stack({ namespace: common_1.NAMESPACE, appName: common_1.OPENCODE_APP, facilities });
10
+ const [dataVolume, dataMounts] = stack.addNFSVolume({
11
+ namePrefix: 'state',
12
+ mountPath: '/data',
13
+ path: facilities.nfs.exportPath,
14
+ server: facilities.nfs.hostname,
15
+ accessMode: 'ReadWriteMany',
16
+ });
17
+ (0, common_1.stripClusterScopedNamespaces)(stack);
18
+ const password = context.secret('opencode-password');
19
+ const credentials = (0, common_1.secretMount)('opencode-password', password);
20
+ const probe = (0, common_1.openCodeHealthProbe)(common_1.OPENCODE_PORT);
21
+ const container = {
22
+ name: 'opencode',
23
+ image: facilities.image('ag-ai-opencode'),
24
+ imagePullPolicy: 'IfNotPresent',
25
+ command: ['opencode', 'serve', '--hostname', '0.0.0.0', '--port', String(common_1.OPENCODE_PORT)],
26
+ ports: [{ name: 'opencode', containerPort: common_1.OPENCODE_PORT, protocol: 'TCP' }],
27
+ env: [
28
+ (0, ag_rpi_mega_1.secretEnv)('OPENCODE_SERVER_PASSWORD', password),
29
+ (0, common_1.env)('HOME', '/data/opencode/home'),
30
+ (0, common_1.env)('XDG_DATA_HOME', '/data/opencode/data'),
31
+ (0, common_1.env)('XDG_CONFIG_HOME', '/data/opencode/config'),
32
+ (0, common_1.env)('XDG_CACHE_HOME', '/data/opencode/cache'),
33
+ ],
34
+ startupProbe: { ...probe, failureThreshold: 30 },
35
+ readinessProbe: { ...probe, initialDelaySeconds: 5 },
36
+ livenessProbe: { ...probe, initialDelaySeconds: 15, periodSeconds: 30 },
37
+ resources: {
38
+ requests: { cpu: '50m', memory: '256Mi' },
39
+ limits: { cpu: '1000m', memory: '1Gi' },
40
+ },
41
+ volumeMounts: [...dataMounts, credentials.mount, { name: 'tmp', mountPath: '/tmp' }],
42
+ };
43
+ stack.addDeployment({
44
+ name: 'server',
45
+ containers: [container],
46
+ volumes: [dataVolume, credentials.volume, { name: 'tmp', emptyDir: { sizeLimit: '256Mi' } }],
47
+ replicas: 1,
48
+ architecture: facilities.arch,
49
+ securityMode: 'None',
50
+ changeNFSFilePerms: true,
51
+ });
52
+ stack.addService({ name: common_1.OPENCODE_SERVICE, tcp: [common_1.OPENCODE_PORT], type: 'ClusterIP' });
53
+ return stack;
54
+ };
55
+ exports.opencodeStack = opencodeStack;
@@ -0,0 +1,4 @@
1
+ import { Stack } from 'ag-rpi-mega';
2
+ import type { ClusterPluginContext } from 'ag-rpi-mega';
3
+ /** Queue worker: OpenCode executor, NFS generation store, LAN-only generation listener. */
4
+ export declare const runnerStack: (context: ClusterPluginContext) => Stack;