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.
- package/README.md +31 -0
- package/dist/client/adapters/cluster.d.ts +12 -0
- package/dist/client/adapters/cluster.js +567 -0
- package/dist/client/adapters/google.d.ts +12 -0
- package/dist/client/adapters/google.js +478 -0
- package/dist/client/client.d.ts +11 -0
- package/dist/client/client.js +192 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +18 -0
- package/dist/client/quota.d.ts +55 -0
- package/dist/client/quota.js +148 -0
- package/dist/client/types.d.ts +124 -0
- package/dist/client/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/internal/apikey.d.ts +5 -0
- package/dist/internal/apikey.js +51 -0
- package/dist/internal/array.d.ts +2 -0
- package/dist/internal/array.js +6 -0
- package/dist/internal/log.d.ts +9 -0
- package/dist/internal/log.js +92 -0
- package/dist/internal/node-cache.d.ts +71 -0
- package/dist/internal/node-cache.js +104 -0
- package/dist/internal/retryOnError.d.ts +26 -0
- package/dist/internal/retryOnError.js +123 -0
- package/dist/internal/string/redact.d.ts +2 -0
- package/dist/internal/string/redact.js +94 -0
- package/dist/internal/string/truncate.d.ts +8 -0
- package/dist/internal/string/truncate.js +16 -0
- package/dist/k8s/assets/network-init.sh +52 -0
- package/dist/k8s/common.d.ts +35 -0
- package/dist/k8s/common.js +56 -0
- package/dist/k8s/index.d.ts +9 -0
- package/dist/k8s/index.js +33 -0
- package/dist/k8s/opencode.d.ts +4 -0
- package/dist/k8s/opencode.js +55 -0
- package/dist/k8s/runner.d.ts +4 -0
- package/dist/k8s/runner.js +247 -0
- package/dist/k8s/t3code.d.ts +4 -0
- package/dist/k8s/t3code.js +60 -0
- package/package.json +56 -0
- package/runtime/bridge.mjs +1093 -0
- package/runtime/executor/opencode.mjs +430 -0
- package/runtime/image/adapter.mjs +213 -0
- package/runtime/index.mjs +53 -0
- package/runtime/protocol.mjs +384 -0
- package/runtime/server.mjs +621 -0
- package/runtime/store.mjs +568 -0
- package/runtime/telemetry.mjs +137 -0
- package/runtime/tests/bridge.test.mjs +554 -0
- package/runtime/tests/image-adapter.test.mjs +148 -0
- package/runtime/tests/protocol.test.mjs +156 -0
- package/runtime/tests/server.test.mjs +231 -0
- package/runtime/tests/store.test.mjs +114 -0
- 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,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,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,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;
|