@capybara-hq/kernel 0.1.0-rc.0 → 0.1.0-rc.1
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/bootstrap.json +10 -6
- package/dist/channels/outbound.js +2 -1
- package/dist/config/schema.d.ts +15 -1
- package/dist/config/schema.js +44 -3
- package/dist/conversations/titles.js +2 -1
- package/dist/ctx/queue.js +3 -2
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +1 -0
- package/dist/evals/worker.js +2 -1
- package/dist/extensions/bootstrap.d.ts +39 -12
- package/dist/extensions/bootstrap.js +132 -38
- package/dist/http/app.js +51 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/kernel.d.ts +12 -9
- package/dist/kernel.js +7 -7
- package/dist/log-config.d.ts +9 -0
- package/dist/log-config.js +27 -0
- package/dist/log-context.d.ts +32 -0
- package/dist/log-context.js +40 -0
- package/dist/log-loki.d.ts +34 -0
- package/dist/log-loki.js +145 -0
- package/dist/log.d.ts +41 -10
- package/dist/log.js +91 -17
- package/dist/net/public-address.js +3 -2
- package/dist/runtime/maintenance.js +2 -1
- package/dist/runtime/run-worker.js +2 -1
- package/dist/runtime/usage-report.js +2 -1
- package/dist/tools/builtins/sandbox/common.d.ts +5 -3
- package/dist/tools/builtins/sandbox/common.js +5 -3
- package/package.json +4 -2
package/dist/log.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { pino } from "pino";
|
|
2
|
+
import { prettyFactory } from "pino-pretty";
|
|
1
3
|
import { secretScalars } from "./config/fields.js";
|
|
4
|
+
import { logContext } from "./log-context.js";
|
|
5
|
+
import { createLokiStream } from "./log-loki.js";
|
|
2
6
|
/** 日志级别,按严重程度递增。 */
|
|
3
7
|
export const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
4
8
|
const LEVELS = LOG_LEVELS;
|
|
5
9
|
const REDACTED = "[REDACTED]";
|
|
6
|
-
const RESERVED = new Set(["
|
|
10
|
+
const RESERVED = new Set(["time", "level", "msg"]);
|
|
7
11
|
/** 部署方密钥的最短登记长度:过短的值打码会误伤普通文本。 */
|
|
8
12
|
const MIN_SECRET_LEN = 4;
|
|
9
13
|
/** 组织来源密钥的最短登记长度。 */
|
|
@@ -101,16 +105,64 @@ function expandSecret(value) {
|
|
|
101
105
|
}
|
|
102
106
|
return out;
|
|
103
107
|
}
|
|
108
|
+
/** Console format: `pretty` (pino-pretty, one line per record), `json`, or `auto` (pretty on a TTY). */
|
|
109
|
+
export const LOG_FORMATS = ["auto", "pretty", "json"];
|
|
110
|
+
/** Fields shown in the pretty prefix instead of the trailing object, in this order. */
|
|
111
|
+
const PREFIX_FIELDS = ["reqId", "traceId", "jobId", "runId", "plugin"];
|
|
112
|
+
/** pino-pretty's message format: `[role] [reqId=…] [plugin=…] msg`, each part only when present. */
|
|
113
|
+
function prettyMessage(log, messageKey) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (typeof log.role === "string")
|
|
116
|
+
parts.push(`[${log.role}]`);
|
|
117
|
+
for (const k of PREFIX_FIELDS) {
|
|
118
|
+
const v = log[k];
|
|
119
|
+
if (typeof v === "string" || typeof v === "number")
|
|
120
|
+
parts.push(`[${k}=${v}]`);
|
|
121
|
+
}
|
|
122
|
+
parts.push(String(log[messageKey] ?? ""));
|
|
123
|
+
return parts.join(" ");
|
|
124
|
+
}
|
|
125
|
+
function consoleStream(opts) {
|
|
126
|
+
if (opts.write) {
|
|
127
|
+
const write = opts.write;
|
|
128
|
+
// pino hands each record as one string ending in "\n"
|
|
129
|
+
return { write: (s) => write(s.endsWith("\n") ? s.slice(0, -1) : s) };
|
|
130
|
+
}
|
|
131
|
+
const format = opts.format ?? "auto";
|
|
132
|
+
if (!LOG_FORMATS.includes(format)) {
|
|
133
|
+
throw new Error(`invalid log.format "${format}" (expected ${LOG_FORMATS.join(", ")})`);
|
|
134
|
+
}
|
|
135
|
+
const out = opts.out ?? process.stdout;
|
|
136
|
+
const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
|
|
137
|
+
const pretty = format === "pretty" || (format === "auto" && isTTY);
|
|
138
|
+
if (!pretty)
|
|
139
|
+
return { write: (s) => out.write(s) };
|
|
140
|
+
// In process (no worker thread): pino-pretty formats each line synchronously.
|
|
141
|
+
const format_ = prettyFactory({
|
|
142
|
+
colorize: isTTY,
|
|
143
|
+
colorizeObjects: isTTY,
|
|
144
|
+
singleLine: true,
|
|
145
|
+
// errors stay inside the one-line object instead of a multi-line stack block
|
|
146
|
+
errorLikeObjectKeys: [],
|
|
147
|
+
translateTime: "SYS:HH:MM:ss.l",
|
|
148
|
+
ignore: ["pid", "hostname", "role", "spanId", ...PREFIX_FIELDS].join(","),
|
|
149
|
+
messageFormat: (log, messageKey) => prettyMessage(log, messageKey),
|
|
150
|
+
});
|
|
151
|
+
return { write: (s) => out.write(format_(s)) };
|
|
152
|
+
}
|
|
104
153
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* `
|
|
154
|
+
* Creates the kernel logger on pino. Each record is `{ time, level, msg, ...base, ...fields }` with
|
|
155
|
+
* `level` the label and `time` ISO 8601; fields cannot overwrite time / level / msg or the base
|
|
156
|
+
* fields; an Error becomes name / message / stack (and cause). Redaction happens before pino, so
|
|
157
|
+
* every output sees the same redacted record: deployment secrets (`addSecrets`) in msg and every
|
|
158
|
+
* field string, org secrets (`addOrgSecrets`) in fields only; level and time are never redacted.
|
|
159
|
+
* The console gets pino-pretty or JSON lines per `opts.format`; `opts.write` replaces the console
|
|
160
|
+
* with a JSON line sink.
|
|
110
161
|
*/
|
|
111
162
|
export function createLogger(level = "info", opts = {}) {
|
|
112
163
|
let current = parseLevel(level);
|
|
113
|
-
const
|
|
164
|
+
const base = opts.base ?? {};
|
|
165
|
+
const reserved = new Set([...RESERVED, ...Object.keys(base)]);
|
|
114
166
|
const orgCapacity = opts.orgSecretCapacity ?? ORG_SECRET_CAPACITY;
|
|
115
167
|
/** 部署方密钥(数量由配置决定,不淘汰)。 */
|
|
116
168
|
const secrets = new Set();
|
|
@@ -136,12 +188,10 @@ export function createLogger(level = "info", opts = {}) {
|
|
|
136
188
|
return deployMatcher(s);
|
|
137
189
|
};
|
|
138
190
|
const isSecret = (v) => secrets.has(v) || orgSecrets.has(v);
|
|
191
|
+
/** Redacts and serializes the fields to JSON (errors expanded, cycles cut). */
|
|
139
192
|
const serialize = (entry) => {
|
|
140
193
|
const seen = new WeakSet();
|
|
141
|
-
return JSON.stringify(entry,
|
|
142
|
-
if (this === entry && RESERVED.has(key)) {
|
|
143
|
-
return key === "msg" && typeof value === "string" ? redactMsg(value) : value;
|
|
144
|
-
}
|
|
194
|
+
return JSON.stringify(entry, (_key, value) => {
|
|
145
195
|
if (typeof value === "string")
|
|
146
196
|
return redact(value);
|
|
147
197
|
if ((typeof value === "number" || typeof value === "bigint") && isSecret(String(value))) {
|
|
@@ -167,21 +217,42 @@ export function createLogger(level = "info", opts = {}) {
|
|
|
167
217
|
return value;
|
|
168
218
|
});
|
|
169
219
|
};
|
|
220
|
+
const loki = opts.loki ? createLokiStream(opts.loki) : undefined;
|
|
221
|
+
const sink = pino({
|
|
222
|
+
level: "debug",
|
|
223
|
+
base,
|
|
224
|
+
messageKey: "msg",
|
|
225
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
226
|
+
formatters: { level: (label) => ({ level: label }) },
|
|
227
|
+
// request / job / trace fields of the current async context (log-context.ts)
|
|
228
|
+
mixin: () => ({ ...logContext() }),
|
|
229
|
+
// fields arrive already serialized: no pino serializers, no depth or size limits
|
|
230
|
+
serializers: { err: (e) => e },
|
|
231
|
+
depthLimit: Number.MAX_SAFE_INTEGER,
|
|
232
|
+
edgeLimit: Number.MAX_SAFE_INTEGER,
|
|
233
|
+
},
|
|
234
|
+
// level filtering is ours (setLevel); every stream takes everything that got this far
|
|
235
|
+
loki
|
|
236
|
+
? pino.multistream([
|
|
237
|
+
{ level: "debug", stream: consoleStream(opts) },
|
|
238
|
+
{ level: "debug", stream: loki },
|
|
239
|
+
])
|
|
240
|
+
: consoleStream(opts));
|
|
170
241
|
const emit = (lvl, msg, fields) => {
|
|
171
242
|
if (LEVELS.indexOf(lvl) < LEVELS.indexOf(current))
|
|
172
243
|
return;
|
|
173
|
-
const entry = {
|
|
244
|
+
const entry = {};
|
|
174
245
|
for (const [k, v] of Object.entries(fields ?? {}))
|
|
175
|
-
if (!
|
|
246
|
+
if (!reserved.has(k))
|
|
176
247
|
entry[k] = v;
|
|
177
|
-
let
|
|
248
|
+
let obj;
|
|
178
249
|
try {
|
|
179
|
-
|
|
250
|
+
obj = JSON.parse(serialize(entry));
|
|
180
251
|
}
|
|
181
252
|
catch {
|
|
182
|
-
|
|
253
|
+
obj = { logError: "unserializable fields" };
|
|
183
254
|
}
|
|
184
|
-
|
|
255
|
+
sink[lvl](obj, redactMsg(msg));
|
|
185
256
|
};
|
|
186
257
|
return {
|
|
187
258
|
debug: (msg, fields) => emit("debug", msg, fields),
|
|
@@ -192,6 +263,9 @@ export function createLogger(level = "info", opts = {}) {
|
|
|
192
263
|
return current;
|
|
193
264
|
},
|
|
194
265
|
redact: (text) => redact(text),
|
|
266
|
+
flush: async () => {
|
|
267
|
+
await loki?.flush();
|
|
268
|
+
},
|
|
195
269
|
setLevel(l) {
|
|
196
270
|
current = parseLevel(l);
|
|
197
271
|
},
|
|
@@ -6,8 +6,9 @@ import { BlockList, isIP } from "node:net";
|
|
|
6
6
|
* checked addresses only (see `safe-fetch.ts`), so a DNS answer that changes between check and
|
|
7
7
|
* connect (DNS rebinding, e.g. `10.0.0.1.nip.io`) cannot reach a private address.
|
|
8
8
|
*
|
|
9
|
-
* The blocked ranges are those of
|
|
10
|
-
*
|
|
9
|
+
* The blocked ranges are those of the sandbox-docker plugin's egress proxy
|
|
10
|
+
* (`plugins/sandbox-docker/egress-proxy/proxy.mjs` in capybara-hq/plugins), ported here because the
|
|
11
|
+
* kernel must not import from a plugin. IPv6 addresses that embed an IPv4 address
|
|
11
12
|
* (v4-mapped `::ffff:0:0/96`, v4-compatible `::/96`, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`) are
|
|
12
13
|
* judged by the embedded IPv4 address; anything unparseable is refused. One addition over proxy.mjs:
|
|
13
14
|
* SIIT IPv4-translated addresses (`::ffff:0:a.b.c.d`, `::ffff:0:0:0/96`, RFC 2765) are also judged by
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Queue, Worker } from "bullmq";
|
|
2
2
|
import { sql } from "kysely";
|
|
3
|
+
import { withJobContext } from "../log-context.js";
|
|
3
4
|
import { bullConnection } from "./run-queue.js";
|
|
4
5
|
/**
|
|
5
6
|
* 内核维护队列(不含 `:`):等待超时任务(`wait-timeout`,见 waits.ts)与周期性维护(输出存档清理、
|
|
@@ -104,7 +105,7 @@ export class Maintenance {
|
|
|
104
105
|
if (this.worker || this.closed)
|
|
105
106
|
return;
|
|
106
107
|
this.workerConn = bullConnection(this.d.redisUrl, this.d.log);
|
|
107
|
-
this.worker = new Worker(MAINTENANCE_QUEUE_NAME, (job, token) => this.process(job, token), {
|
|
108
|
+
this.worker = new Worker(MAINTENANCE_QUEUE_NAME, (job, token) => withJobContext(MAINTENANCE_QUEUE_NAME, job, () => this.process(job, token)), {
|
|
108
109
|
connection: this.workerConn,
|
|
109
110
|
concurrency: 5,
|
|
110
111
|
});
|
|
@@ -4,6 +4,7 @@ import { sql } from "kysely";
|
|
|
4
4
|
import { normalizeStoredDefinition } from "../agents/definition.js";
|
|
5
5
|
import { effectiveDefinition, NO_SELECTION, readSelection, sameModel, sameSelection, } from "../conversations/selection.js";
|
|
6
6
|
import { ApiError } from "../errors.js";
|
|
7
|
+
import { withJobContext } from "../log-context.js";
|
|
7
8
|
import { pinnedFailureText, recordSkillsCatalog } from "../skills/catalog.js";
|
|
8
9
|
import { RunError } from "./model-call.js";
|
|
9
10
|
import { ProviderListError } from "./providers.js";
|
|
@@ -60,7 +61,7 @@ export class RunWorker {
|
|
|
60
61
|
const runtime = this.d.config().runtime;
|
|
61
62
|
this.conn = bullConnection(this.d.redisUrl, this.d.log);
|
|
62
63
|
for (const queue of [RUN_QUEUE_NAME, SUBAGENT_RUN_QUEUE_NAME]) {
|
|
63
|
-
const worker = new Worker(queue, (job, token) => this.process(job, token), {
|
|
64
|
+
const worker = new Worker(queue, (job, token) => withJobContext(queue, job, () => this.process(job, token)), {
|
|
64
65
|
// the worker name is recorded on the job (BullMQ processedBy): which replica ran it
|
|
65
66
|
name: `${hostname()}-${process.pid}`,
|
|
66
67
|
connection: this.conn,
|
|
@@ -2,6 +2,7 @@ import { DelayedError, Job, Queue, UnrecoverableError, Worker } from "bullmq";
|
|
|
2
2
|
import { sql } from "kysely";
|
|
3
3
|
import { enabledExtensionsQuery } from "../extensions/enabled.js";
|
|
4
4
|
import { deepFreeze, errMsg } from "../extensions/hooks.js";
|
|
5
|
+
import { withJobContext } from "../log-context.js";
|
|
5
6
|
import { bullConnection } from "./run-queue.js";
|
|
6
7
|
/** Queue of `usage.report` deliveries (no `:` in the name). */
|
|
7
8
|
export const USAGE_REPORT_QUEUE = "cap-kernel-usage-report";
|
|
@@ -256,7 +257,7 @@ export class UsageReporter {
|
|
|
256
257
|
// replica that never enqueued (m-b)
|
|
257
258
|
this.getQueue();
|
|
258
259
|
this.workerConn = bullConnection(this.d.redisUrl, this.d.log);
|
|
259
|
-
this.worker = new Worker(USAGE_REPORT_QUEUE, (j, t) => this.process(j, t), {
|
|
260
|
+
this.worker = new Worker(USAGE_REPORT_QUEUE, (j, t) => withJobContext(USAGE_REPORT_QUEUE, j, () => this.process(j, t)), {
|
|
260
261
|
connection: this.workerConn,
|
|
261
262
|
concurrency: this.d.config().hooks.usageReportConcurrency,
|
|
262
263
|
lockDuration: jobLockMs,
|
|
@@ -37,9 +37,11 @@ export declare const SYNC_MARKER = "/workspace/attachments/.capybara-sync.json";
|
|
|
37
37
|
/** 附件文件名的上限(UTF-8 字节,常见文件系统单个文件名上限 255 字节,留出 `-N` 后缀的余量)。 */
|
|
38
38
|
export declare const ATTACHMENT_NAME_MAX_BYTES = 200;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Extra margin (ms) on top of a sandbox tool's time limit: creating / resuming the sandbox, and the
|
|
41
|
+
* fallback for stopping a command after its timeout (when the E2B kill script does not finish, the
|
|
42
|
+
* sandbox is restarted: up to about 20 s to kill, plus pause and a cold connect of up to 120 s, about
|
|
43
|
+
* 160 s). Pulling the image on first creation is not included (deployments pre-pull
|
|
44
|
+
* `sandbox.image`; see the sandbox-docker plugin's README in capybara-hq/plugins).
|
|
43
45
|
*/
|
|
44
46
|
export declare const SANDBOX_SETUP_MARGIN_MS = 180000;
|
|
45
47
|
/**
|
|
@@ -22,9 +22,11 @@ const MARKER_MAX_BYTES = 1024 * 1024;
|
|
|
22
22
|
/** 附件文件名的上限(UTF-8 字节,常见文件系统单个文件名上限 255 字节,留出 `-N` 后缀的余量)。 */
|
|
23
23
|
export const ATTACHMENT_NAME_MAX_BYTES = 200;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Extra margin (ms) on top of a sandbox tool's time limit: creating / resuming the sandbox, and the
|
|
26
|
+
* fallback for stopping a command after its timeout (when the E2B kill script does not finish, the
|
|
27
|
+
* sandbox is restarted: up to about 20 s to kill, plus pause and a cold connect of up to 120 s, about
|
|
28
|
+
* 160 s). Pulling the image on first creation is not included (deployments pre-pull
|
|
29
|
+
* `sandbox.image`; see the sandbox-docker plugin's README in capybara-hq/plugins).
|
|
28
30
|
*/
|
|
29
31
|
export const SANDBOX_SETUP_MARGIN_MS = 180_000;
|
|
30
32
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/kernel",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
4
|
"description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"jose": "^6.2.12",
|
|
48
48
|
"kysely": "^0.29.6",
|
|
49
49
|
"pg": "^8.23.1",
|
|
50
|
+
"pino": "10.4.0",
|
|
51
|
+
"pino-pretty": "13.2.0",
|
|
50
52
|
"reflect-metadata": "0.2.2",
|
|
51
53
|
"rxjs": "^7.8.2",
|
|
52
54
|
"semver": "^7.8.5",
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"@types/pg": "^8.23.1",
|
|
63
65
|
"@types/semver": "^7.8.0",
|
|
64
66
|
"@types/yauzl": "3.4.0",
|
|
65
|
-
"@capybara-hq/testing": "0.1.0-rc.
|
|
67
|
+
"@capybara-hq/testing": "0.1.0-rc.2"
|
|
66
68
|
},
|
|
67
69
|
"scripts": {
|
|
68
70
|
"build": "tsc -b"
|