pi-harness-runtime 1.1.396 → 1.1.398
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/AGENTS.md +54 -7
- package/harness/notification-events.ts +6 -6
- package/index.ts +1 -1
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/README.md +41 -6
- package/packages/logging/dist/classification.d.ts +65 -0
- package/packages/logging/dist/classification.d.ts.map +1 -0
- package/packages/logging/dist/classification.js +155 -0
- package/packages/logging/dist/classification.js.map +1 -0
- package/packages/logging/dist/config.d.ts +9 -0
- package/packages/logging/dist/config.d.ts.map +1 -1
- package/packages/logging/dist/config.js +40 -3
- package/packages/logging/dist/config.js.map +1 -1
- package/packages/logging/dist/index.d.ts +9 -1
- package/packages/logging/dist/index.d.ts.map +1 -1
- package/packages/logging/dist/index.js +10 -1
- package/packages/logging/dist/index.js.map +1 -1
- package/packages/logging/dist/ingest/schema.d.ts +60 -0
- package/packages/logging/dist/ingest/schema.d.ts.map +1 -0
- package/packages/logging/dist/ingest/schema.js +245 -0
- package/packages/logging/dist/ingest/schema.js.map +1 -0
- package/packages/logging/dist/installation.d.ts +32 -0
- package/packages/logging/dist/installation.d.ts.map +1 -0
- package/packages/logging/dist/installation.js +64 -0
- package/packages/logging/dist/installation.js.map +1 -0
- package/packages/logging/dist/sensitive-keys.d.ts +12 -0
- package/packages/logging/dist/sensitive-keys.d.ts.map +1 -0
- package/packages/logging/dist/sensitive-keys.js +39 -0
- package/packages/logging/dist/sensitive-keys.js.map +1 -0
- package/packages/logging/dist/server/credentials.d.ts +85 -0
- package/packages/logging/dist/server/credentials.d.ts.map +1 -0
- package/packages/logging/dist/server/credentials.js +147 -0
- package/packages/logging/dist/server/credentials.js.map +1 -0
- package/packages/logging/dist/server/http.d.ts +36 -0
- package/packages/logging/dist/server/http.d.ts.map +1 -0
- package/packages/logging/dist/server/http.js +139 -0
- package/packages/logging/dist/server/http.js.map +1 -0
- package/packages/logging/dist/server/index-store.d.ts +95 -0
- package/packages/logging/dist/server/index-store.d.ts.map +1 -0
- package/packages/logging/dist/server/index-store.js +190 -0
- package/packages/logging/dist/server/index-store.js.map +1 -0
- package/packages/logging/dist/server/index.d.ts +22 -0
- package/packages/logging/dist/server/index.d.ts.map +1 -0
- package/packages/logging/dist/server/index.js +15 -0
- package/packages/logging/dist/server/index.js.map +1 -0
- package/packages/logging/dist/server/keys.d.ts +22 -0
- package/packages/logging/dist/server/keys.d.ts.map +1 -0
- package/packages/logging/dist/server/keys.js +24 -0
- package/packages/logging/dist/server/keys.js.map +1 -0
- package/packages/logging/dist/server/outbox-worker.d.ts +55 -0
- package/packages/logging/dist/server/outbox-worker.d.ts.map +1 -0
- package/packages/logging/dist/server/outbox-worker.js +95 -0
- package/packages/logging/dist/server/outbox-worker.js.map +1 -0
- package/packages/logging/dist/server/outbox.d.ts +112 -0
- package/packages/logging/dist/server/outbox.d.ts.map +1 -0
- package/packages/logging/dist/server/outbox.js +247 -0
- package/packages/logging/dist/server/outbox.js.map +1 -0
- package/packages/logging/dist/server/service.d.ts +83 -0
- package/packages/logging/dist/server/service.d.ts.map +1 -0
- package/packages/logging/dist/server/service.js +144 -0
- package/packages/logging/dist/server/service.js.map +1 -0
- package/packages/logging/dist/shipper.d.ts +20 -4
- package/packages/logging/dist/shipper.d.ts.map +1 -1
- package/packages/logging/dist/shipper.js +31 -5
- package/packages/logging/dist/shipper.js.map +1 -1
- package/packages/logging/dist/singleton.d.ts.map +1 -1
- package/packages/logging/dist/singleton.js +3 -0
- package/packages/logging/dist/singleton.js.map +1 -1
- package/packages/logging/dist/types.d.ts +28 -1
- package/packages/logging/dist/types.d.ts.map +1 -1
- package/packages/logging/dist/types.js +8 -0
- package/packages/logging/dist/types.js.map +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/logging/src/classification.ts +175 -0
- package/packages/logging/src/config.ts +60 -3
- package/packages/logging/src/index.ts +45 -1
- package/packages/logging/src/ingest/schema.ts +295 -0
- package/packages/logging/src/installation.ts +76 -0
- package/packages/logging/src/sensitive-keys.ts +40 -0
- package/packages/logging/src/server/credentials.ts +198 -0
- package/packages/logging/src/server/http.ts +170 -0
- package/packages/logging/src/server/index-store.ts +242 -0
- package/packages/logging/src/server/index.ts +56 -0
- package/packages/logging/src/server/keys.ts +32 -0
- package/packages/logging/src/server/outbox-worker.ts +124 -0
- package/packages/logging/src/server/outbox.ts +316 -0
- package/packages/logging/src/server/service.ts +196 -0
- package/packages/logging/src/shipper.ts +54 -8
- package/packages/logging/src/singleton.ts +3 -0
- package/packages/logging/src/types.ts +29 -1
- package/packages/logging/test/classification.test.ts +137 -0
- package/packages/logging/test/identity-transport.test.ts +197 -0
- package/packages/logging/test/ingest-schema.test.ts +184 -0
- package/packages/logging/test/integration.test.ts +9 -4
- package/packages/logging/test/log-http.test.ts +246 -0
- package/packages/logging/test/server.test.ts +444 -0
- package/packages/logging/test/shipper.test.ts +11 -11
- package/packages/logging/test/spool.test.ts +4 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/detail-format.d.ts +34 -0
- package/packages/notification/dist/adapters/detail-format.d.ts.map +1 -0
- package/packages/notification/dist/adapters/detail-format.js +55 -0
- package/packages/notification/dist/adapters/detail-format.js.map +1 -0
- package/packages/notification/dist/adapters/email-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/email-adapter.js +3 -2
- package/packages/notification/dist/adapters/email-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.js +3 -2
- package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/ntfy-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/ntfy-adapter.js +3 -2
- package/packages/notification/dist/adapters/ntfy-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.d.ts +14 -0
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +83 -67
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +1 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/types.d.ts +3 -0
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/detail-format.ts +74 -0
- package/packages/notification/src/adapters/email-adapter.ts +3 -2
- package/packages/notification/src/adapters/line-adapter.ts +3 -2
- package/packages/notification/src/adapters/ntfy-adapter.ts +3 -2
- package/packages/notification/src/adapters/telegram-adapter.ts +92 -74
- package/packages/notification/src/notification-center.ts +1 -0
- package/packages/notification/src/types.ts +3 -0
- package/packages/notification/test/detail-format.test.ts +106 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/unified-webhook-server.ts +86 -81
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What may leave the machine.
|
|
3
|
+
*
|
|
4
|
+
* The local log file keeps everything. The centralized archive keeps what an
|
|
5
|
+
* operator actually needs: service state, event codes, sanitized errors,
|
|
6
|
+
* durations and opaque identifiers.
|
|
7
|
+
*
|
|
8
|
+
* Two rules decide:
|
|
9
|
+
*
|
|
10
|
+
* 1. The event decides. A catch-all mirror of a local log line
|
|
11
|
+
* (`harness.log`) or anything that smells like a prompt, a response or
|
|
12
|
+
* command output never leaves the machine.
|
|
13
|
+
* 2. The payload decides. A message only travels when it looks like a
|
|
14
|
+
* sanitized summary: short, one line, no path and no URL with a query
|
|
15
|
+
* string. Fields with a private key name are dropped.
|
|
16
|
+
*
|
|
17
|
+
* The diagnostic mode (`PI_LOG_DIAGNOSTIC=1`) relaxes rule 2 for a user who
|
|
18
|
+
* explicitly asked for more, and nothing else.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { isLocalOnlyFieldKey } from "./sensitive-keys.js";
|
|
22
|
+
import type { LogRecord } from "./types.js";
|
|
23
|
+
|
|
24
|
+
export interface ClassificationPolicy {
|
|
25
|
+
/** Events that may go remote. */
|
|
26
|
+
remoteEvents: RegExp[];
|
|
27
|
+
/** Events that never go remote, whatever the level is. */
|
|
28
|
+
localEvents: RegExp[];
|
|
29
|
+
/** Keep a message even when it does not look sanitized. */
|
|
30
|
+
allowRawMessages: boolean;
|
|
31
|
+
/** Maximum length of a message that is kept. */
|
|
32
|
+
maxMessageLength: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Events that stay local.
|
|
37
|
+
*
|
|
38
|
+
* `harness.log` is the mirror of the catch-all local log line - it carries
|
|
39
|
+
* whatever the harness printed, so it is never remote.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_LOCAL_EVENTS: RegExp[] = [
|
|
42
|
+
/^harness\.log$/,
|
|
43
|
+
/prompt/i,
|
|
44
|
+
/response/i,
|
|
45
|
+
/transcript/i,
|
|
46
|
+
/output/i,
|
|
47
|
+
/command/i,
|
|
48
|
+
/content/i,
|
|
49
|
+
/message\.text/i,
|
|
50
|
+
/notification\.text/i,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Events that may go remote.
|
|
55
|
+
*
|
|
56
|
+
* They are structured: a service state change, a session lifecycle event, a
|
|
57
|
+
* validation tier, a provider or quota event.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_REMOTE_EVENTS: RegExp[] = [
|
|
60
|
+
/^harness\.service-diagnostics\./,
|
|
61
|
+
/^harness\.session\./,
|
|
62
|
+
/^harness\.startup\./,
|
|
63
|
+
/^harness\.shutdown\./,
|
|
64
|
+
/^harness\.planner\.decision$/,
|
|
65
|
+
/^harness\.glm-quota\.quota$/,
|
|
66
|
+
/^validation\./,
|
|
67
|
+
/^tier\./,
|
|
68
|
+
/^service\./,
|
|
69
|
+
/^session\./,
|
|
70
|
+
/^startup\./,
|
|
71
|
+
/^config\./,
|
|
72
|
+
/^provider\./,
|
|
73
|
+
/^quota\./,
|
|
74
|
+
/^logging\./,
|
|
75
|
+
/^jest\./,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_CLASSIFICATION_POLICY: ClassificationPolicy = {
|
|
79
|
+
remoteEvents: DEFAULT_REMOTE_EVENTS,
|
|
80
|
+
localEvents: DEFAULT_LOCAL_EVENTS,
|
|
81
|
+
allowRawMessages: false,
|
|
82
|
+
maxMessageLength: 200,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ABSOLUTE_PATH = /(?:^|\s)\/(?:[\w.-]+\/)+[\w.-]+/;
|
|
86
|
+
const URL_WITH_QUERY = /https?:\/\/\S+\?\S+/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True when a message looks like a sanitized summary and may travel.
|
|
90
|
+
*/
|
|
91
|
+
export function isSanitizedMessage(message: string, policy: ClassificationPolicy): boolean {
|
|
92
|
+
if (policy.allowRawMessages) return true;
|
|
93
|
+
if (message.length > policy.maxMessageLength) return false;
|
|
94
|
+
if (message.includes("\n")) return false;
|
|
95
|
+
if (ABSOLUTE_PATH.test(message)) return false;
|
|
96
|
+
if (URL_WITH_QUERY.test(message)) return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Classify one record.
|
|
102
|
+
*
|
|
103
|
+
* "remote" means the record may be shipped; "local" means it must stay on
|
|
104
|
+
* the machine.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyRecord(
|
|
107
|
+
record: LogRecord,
|
|
108
|
+
policy: ClassificationPolicy = DEFAULT_CLASSIFICATION_POLICY,
|
|
109
|
+
): "remote" | "local" {
|
|
110
|
+
for (const pattern of policy.localEvents) {
|
|
111
|
+
if (pattern.test(record.event)) return "local";
|
|
112
|
+
}
|
|
113
|
+
for (const pattern of policy.remoteEvents) {
|
|
114
|
+
if (pattern.test(record.event)) return "remote";
|
|
115
|
+
}
|
|
116
|
+
// Errors stay valuable even when the event is unknown: the message and
|
|
117
|
+
// the fields are sanitized anyway.
|
|
118
|
+
if (record.level === "warn" || record.level === "error") return "remote";
|
|
119
|
+
// Unknown events are local by default: an allowlist is safer than a
|
|
120
|
+
// denylist.
|
|
121
|
+
return "local";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the version of a record that may leave the machine.
|
|
126
|
+
*
|
|
127
|
+
* Returns null when the record must stay local.
|
|
128
|
+
*/
|
|
129
|
+
export function prepareRemoteRecord(
|
|
130
|
+
record: LogRecord,
|
|
131
|
+
policy: ClassificationPolicy = DEFAULT_CLASSIFICATION_POLICY,
|
|
132
|
+
): LogRecord | null {
|
|
133
|
+
if (classifyRecord(record, policy) === "local") return null;
|
|
134
|
+
|
|
135
|
+
const remote: LogRecord = {
|
|
136
|
+
ts: record.ts,
|
|
137
|
+
level: record.level,
|
|
138
|
+
source: record.source,
|
|
139
|
+
event: record.event,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (record.message !== undefined && isSanitizedMessage(record.message, policy)) {
|
|
143
|
+
remote.message = record.message;
|
|
144
|
+
}
|
|
145
|
+
if (record.sessionId) remote.sessionId = record.sessionId;
|
|
146
|
+
if (typeof record.durationMs === "number") remote.durationMs = record.durationMs;
|
|
147
|
+
|
|
148
|
+
if (record.fields) {
|
|
149
|
+
const fields: Record<string, unknown> = {};
|
|
150
|
+
for (const [key, value] of Object.entries(record.fields)) {
|
|
151
|
+
if (isLocalOnlyFieldKey(key)) continue;
|
|
152
|
+
fields[key] = value;
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(fields).length > 0) remote.fields = fields;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (record.error) {
|
|
158
|
+
remote.error = { ...record.error };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return remote;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Keep only the records that may leave the machine. */
|
|
165
|
+
export function filterRemoteRecords(
|
|
166
|
+
records: LogRecord[],
|
|
167
|
+
policy: ClassificationPolicy = DEFAULT_CLASSIFICATION_POLICY,
|
|
168
|
+
): LogRecord[] {
|
|
169
|
+
const remote: LogRecord[] = [];
|
|
170
|
+
for (const record of records) {
|
|
171
|
+
const prepared = prepareRemoteRecord(record, policy);
|
|
172
|
+
if (prepared) remote.push(prepared);
|
|
173
|
+
}
|
|
174
|
+
return remote;
|
|
175
|
+
}
|
|
@@ -13,6 +13,11 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { createR2Transport } from "./transports/r2.js";
|
|
15
15
|
import { createWebhookTransport } from "./transports/webhook.js";
|
|
16
|
+
import { resolveInstallationId } from "./installation.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_CLASSIFICATION_POLICY,
|
|
19
|
+
type ClassificationPolicy,
|
|
20
|
+
} from "./classification.js";
|
|
16
21
|
import type { LogBufferOptions } from "./buffer.js";
|
|
17
22
|
import type { SamplingPolicy } from "./sampler.js";
|
|
18
23
|
import type { LogTransport } from "./types.js";
|
|
@@ -24,6 +29,10 @@ export interface LogSinkConfig {
|
|
|
24
29
|
enabled: boolean;
|
|
25
30
|
/** Application name reported with every batch. */
|
|
26
31
|
app: string;
|
|
32
|
+
/** Version of the emitting application. */
|
|
33
|
+
appVersion?: string;
|
|
34
|
+
/** Stable id of this installation. */
|
|
35
|
+
installationId: string;
|
|
27
36
|
/** Transports in delivery priority order. */
|
|
28
37
|
transports: LogTransport[];
|
|
29
38
|
/** Directory of the local spool (undefined when logging is off). */
|
|
@@ -31,6 +40,8 @@ export interface LogSinkConfig {
|
|
|
31
40
|
sampling: Partial<SamplingPolicy>;
|
|
32
41
|
buffer: LogBufferOptions;
|
|
33
42
|
flushIntervalMs: number;
|
|
43
|
+
/** What may leave the machine. */
|
|
44
|
+
classification: ClassificationPolicy;
|
|
34
45
|
/** Human readable notes about what was enabled and why. */
|
|
35
46
|
notes: string[];
|
|
36
47
|
}
|
|
@@ -39,6 +50,8 @@ export interface ResolveConfigOptions {
|
|
|
39
50
|
env?: Record<string, string | undefined>;
|
|
40
51
|
/** Directory holding the credential key files. */
|
|
41
52
|
keysDir?: string;
|
|
53
|
+
/** Overrides the installation id (tests, containers). */
|
|
54
|
+
installationId?: string;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
function readKeyFile(keysDir: string, name: string): string | undefined {
|
|
@@ -67,6 +80,21 @@ export function defaultSpoolDir(): string {
|
|
|
67
80
|
return join(homedir(), ".pi-harness-runtime", "logs", "spool");
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Build the classification policy.
|
|
85
|
+
*
|
|
86
|
+
* `PI_LOG_DIAGNOSTIC=1` is the explicit, user controlled diagnostic mode: it
|
|
87
|
+
* lets messages travel that the default policy keeps local. It never widens
|
|
88
|
+
* the event allowlist - only the message rule.
|
|
89
|
+
*/
|
|
90
|
+
function classificationFor(
|
|
91
|
+
env: Record<string, string | undefined>,
|
|
92
|
+
): ClassificationPolicy {
|
|
93
|
+
const diagnostic = env.PI_LOG_DIAGNOSTIC === "1" || env.PI_LOG_DIAGNOSTIC === "true";
|
|
94
|
+
if (!diagnostic) return DEFAULT_CLASSIFICATION_POLICY;
|
|
95
|
+
return { ...DEFAULT_CLASSIFICATION_POLICY, allowRawMessages: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
/**
|
|
71
99
|
* Build the log sink configuration from the environment and the key files.
|
|
72
100
|
*/
|
|
@@ -76,14 +104,19 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
76
104
|
const mode = (env.PI_LOG_SINK ?? "auto") as LogSinkMode;
|
|
77
105
|
const notes: string[] = [];
|
|
78
106
|
|
|
107
|
+
const installationId =
|
|
108
|
+
options.installationId ?? env.PI_LOG_INSTALLATION_ID ?? resolveInstallationId();
|
|
109
|
+
|
|
79
110
|
if (mode === "off") {
|
|
80
111
|
return {
|
|
81
112
|
enabled: false,
|
|
82
113
|
app: env.PI_LOG_APP ?? "pi-harness",
|
|
114
|
+
installationId,
|
|
83
115
|
transports: [],
|
|
84
116
|
sampling: {},
|
|
85
117
|
buffer: {},
|
|
86
118
|
flushIntervalMs: 0,
|
|
119
|
+
classification: classificationFor(env),
|
|
87
120
|
notes: ["logging disabled by PI_LOG_SINK=off"],
|
|
88
121
|
};
|
|
89
122
|
}
|
|
@@ -102,7 +135,21 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
102
135
|
(accountId ? `https://${accountId}.r2.cloudflarestorage.com` : undefined);
|
|
103
136
|
|
|
104
137
|
const r2Ready = Boolean(bucket && accessKeyId && secretAccessKey && endpoint);
|
|
105
|
-
const
|
|
138
|
+
const webhookUrl = env.PI_LOG_WEBHOOK_URL;
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* Transport selection.
|
|
142
|
+
*
|
|
143
|
+
* The transports form a FALLBACK chain, not a broadcast: the first
|
|
144
|
+
* transport that accepts the batch wins and the later ones are not
|
|
145
|
+
* called. `auto` therefore prefers the webhook ingress when one is
|
|
146
|
+
* configured: the server is the single writer to R2, so client machines
|
|
147
|
+
* never need R2 credentials at all.
|
|
148
|
+
*/
|
|
149
|
+
const wantR2 =
|
|
150
|
+
mode === "r2" ||
|
|
151
|
+
mode === "r2+webhook" ||
|
|
152
|
+
(mode === "auto" && r2Ready && !webhookUrl);
|
|
106
153
|
|
|
107
154
|
if (wantR2) {
|
|
108
155
|
if (r2Ready) {
|
|
@@ -121,7 +168,6 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
121
168
|
}
|
|
122
169
|
}
|
|
123
170
|
|
|
124
|
-
const webhookUrl = env.PI_LOG_WEBHOOK_URL;
|
|
125
171
|
const wantWebhook =
|
|
126
172
|
mode === "webhook" || mode === "r2+webhook" || (mode === "auto" && Boolean(webhookUrl));
|
|
127
173
|
|
|
@@ -134,6 +180,11 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
134
180
|
}),
|
|
135
181
|
);
|
|
136
182
|
notes.push(`webhook enabled (${webhookUrl})`);
|
|
183
|
+
if (mode === "auto") {
|
|
184
|
+
notes.push(
|
|
185
|
+
"webhook is the client ingress point - the server writes to R2 (direct r2 transport skipped)",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
137
188
|
} else {
|
|
138
189
|
notes.push("webhook requested but PI_LOG_WEBHOOK_URL is not set - transport skipped");
|
|
139
190
|
}
|
|
@@ -143,9 +194,10 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
143
194
|
notes.push("no transport configured - logs stay local");
|
|
144
195
|
}
|
|
145
196
|
|
|
146
|
-
|
|
197
|
+
const config: LogSinkConfig = {
|
|
147
198
|
enabled: true,
|
|
148
199
|
app: env.PI_LOG_APP ?? "pi-harness",
|
|
200
|
+
installationId,
|
|
149
201
|
transports,
|
|
150
202
|
spoolDir: env.PI_LOG_SPOOL_DIR ?? defaultSpoolDir(),
|
|
151
203
|
sampling: {
|
|
@@ -158,6 +210,11 @@ export function resolveLogSinkConfig(options: ResolveConfigOptions = {}): LogSin
|
|
|
158
210
|
maxBatchSize: readNumber(env, "PI_LOG_BATCH_SIZE", 100),
|
|
159
211
|
},
|
|
160
212
|
flushIntervalMs: readNumber(env, "PI_LOG_FLUSH_INTERVAL_MS", 15_000),
|
|
213
|
+
classification: classificationFor(env),
|
|
161
214
|
notes,
|
|
162
215
|
};
|
|
216
|
+
|
|
217
|
+
if (env.PI_LOG_APP_VERSION) config.appVersion = env.PI_LOG_APP_VERSION;
|
|
218
|
+
|
|
219
|
+
return config;
|
|
163
220
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
// --- Types -----------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
export { LOG_LEVEL_SEVERITY } from "./types.js";
|
|
15
|
+
export { LOG_LEVEL_SEVERITY, LOG_SCHEMA_VERSION } from "./types.js";
|
|
16
16
|
export type {
|
|
17
17
|
FlushStats,
|
|
18
18
|
LogBatch,
|
|
@@ -77,6 +77,50 @@ export {
|
|
|
77
77
|
} from "./singleton.js";
|
|
78
78
|
export type { InitCentralLoggingOptions } from "./singleton.js";
|
|
79
79
|
|
|
80
|
+
// --- Installation identity --------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
INSTALLATION_ID_FILE,
|
|
84
|
+
defaultInstallationIdPath,
|
|
85
|
+
isValidInstallationId,
|
|
86
|
+
resolveInstallationId,
|
|
87
|
+
} from "./installation.js";
|
|
88
|
+
export type { ResolveInstallationIdOptions } from "./installation.js";
|
|
89
|
+
|
|
90
|
+
// --- Classification (what may leave the machine) ----------------------------
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
DEFAULT_CLASSIFICATION_POLICY,
|
|
94
|
+
DEFAULT_LOCAL_EVENTS,
|
|
95
|
+
DEFAULT_REMOTE_EVENTS,
|
|
96
|
+
classifyRecord,
|
|
97
|
+
filterRemoteRecords,
|
|
98
|
+
isSanitizedMessage,
|
|
99
|
+
prepareRemoteRecord,
|
|
100
|
+
} from "./classification.js";
|
|
101
|
+
export type { ClassificationPolicy } from "./classification.js";
|
|
102
|
+
|
|
103
|
+
export { LOCAL_ONLY_FIELD_KEYS, isLocalOnlyFieldKey } from "./sensitive-keys.js";
|
|
104
|
+
|
|
105
|
+
// --- Ingest contract (shared by client and server) -------------------------
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
DEFAULT_BATCH_LIMITS,
|
|
109
|
+
LOG_LEVELS,
|
|
110
|
+
sanitizeRecord,
|
|
111
|
+
validateBatch,
|
|
112
|
+
} from "./ingest/schema.js";
|
|
113
|
+
export type {
|
|
114
|
+
BatchValidationError,
|
|
115
|
+
BatchValidationLimits,
|
|
116
|
+
BatchValidationResult,
|
|
117
|
+
SanitizeRecordResult,
|
|
118
|
+
} from "./ingest/schema.js";
|
|
119
|
+
|
|
120
|
+
// --- Server side ------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
export * from "./server/index.js";
|
|
123
|
+
|
|
80
124
|
// --- Configuration ---------------------------------------------------------
|
|
81
125
|
|
|
82
126
|
export {
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest contract: strict validation and allowlisted redaction.
|
|
3
|
+
*
|
|
4
|
+
* The client and the server share this module, so both sides agree on what a
|
|
5
|
+
* batch is. The server never trusts the body:
|
|
6
|
+
*
|
|
7
|
+
* - the tenant comes from the authenticated credential, never from the JSON,
|
|
8
|
+
* - the batch is validated against a strict, versioned schema,
|
|
9
|
+
* - the records are rebuilt from an allowlist, so an unknown property can
|
|
10
|
+
* never smuggle data into the archive,
|
|
11
|
+
* - every string is redacted again on the server (defence in depth).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { redactText, redactValue } from "../redact.js";
|
|
15
|
+
import { isLocalOnlyFieldKey } from "../sensitive-keys.js";
|
|
16
|
+
import { LOG_SCHEMA_VERSION, type LogBatch, type LogErrorInfo, type LogLevel, type LogRecord } from "../types.js";
|
|
17
|
+
|
|
18
|
+
export const LOG_LEVELS: readonly LogLevel[] = ["debug", "info", "warn", "error"];
|
|
19
|
+
|
|
20
|
+
export interface BatchValidationLimits {
|
|
21
|
+
/** Maximum number of records in one batch. */
|
|
22
|
+
maxRecords: number;
|
|
23
|
+
/** Maximum length of a single string value. */
|
|
24
|
+
maxStringLength: number;
|
|
25
|
+
/** Maximum number of structured fields per record. */
|
|
26
|
+
maxFieldKeys: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_BATCH_LIMITS: BatchValidationLimits = {
|
|
30
|
+
maxRecords: 1000,
|
|
31
|
+
maxStringLength: 8192,
|
|
32
|
+
maxFieldKeys: 64,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface BatchValidationError {
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type BatchValidationResult =
|
|
41
|
+
| { ok: true; batch: LogBatch; warnings: string[] }
|
|
42
|
+
| { ok: false; error: BatchValidationError };
|
|
43
|
+
|
|
44
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isIsoTimestamp(value: string): boolean {
|
|
49
|
+
return Number.isFinite(Date.parse(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Batch ids are opaque but must stay safe to use in an object key. */
|
|
53
|
+
const BATCH_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
54
|
+
const INSTALLATION_ID_PATTERN = /^[A-Za-z0-9._:-]{8,64}$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate an inbound batch.
|
|
58
|
+
*
|
|
59
|
+
* Unknown properties are NOT an error here - the sanitizer drops them. Only a
|
|
60
|
+
* structurally broken batch is rejected.
|
|
61
|
+
*/
|
|
62
|
+
export function validateBatch(
|
|
63
|
+
input: unknown,
|
|
64
|
+
options: { limits?: Partial<BatchValidationLimits> } = {},
|
|
65
|
+
): BatchValidationResult {
|
|
66
|
+
const limits = { ...DEFAULT_BATCH_LIMITS, ...options.limits };
|
|
67
|
+
const warnings: string[] = [];
|
|
68
|
+
|
|
69
|
+
if (!isPlainObject(input)) {
|
|
70
|
+
return { ok: false, error: { code: "invalid_batch", message: "batch must be an object" } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let schemaVersion = input.schemaVersion;
|
|
74
|
+
if (schemaVersion === undefined) {
|
|
75
|
+
// A client that predates the version marker sends v1.
|
|
76
|
+
schemaVersion = 1;
|
|
77
|
+
warnings.push("batch without a schema version, treated as legacy v1");
|
|
78
|
+
}
|
|
79
|
+
if (typeof schemaVersion !== "number" || !Number.isFinite(schemaVersion)) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: { code: "invalid_schema_version", message: "schemaVersion must be a number" },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (schemaVersion < 1) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: { code: "invalid_schema_version", message: "schemaVersion must be >= 1" },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (schemaVersion > LOG_SCHEMA_VERSION) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: {
|
|
95
|
+
code: "unsupported_schema_version",
|
|
96
|
+
message: `schemaVersion ${schemaVersion} is newer than the supported version ${LOG_SCHEMA_VERSION}`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (schemaVersion < LOG_SCHEMA_VERSION) {
|
|
101
|
+
warnings.push(`legacy schemaVersion ${schemaVersion}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const batchId = input.batchId;
|
|
105
|
+
if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId)) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: { code: "invalid_batch_id", message: "batchId is missing or malformed" },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const app = input.app;
|
|
113
|
+
if (typeof app !== "string" || app.length === 0 || app.length > 64) {
|
|
114
|
+
return { ok: false, error: { code: "invalid_app", message: "app is missing or malformed" } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const installationId = input.installationId;
|
|
118
|
+
if (installationId !== undefined && typeof installationId !== "string") {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code: "invalid_installation_id", message: "installationId must be a string" },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (schemaVersion >= 2) {
|
|
125
|
+
if (typeof installationId !== "string" || !INSTALLATION_ID_PATTERN.test(installationId)) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: { code: "invalid_installation_id", message: "installationId is missing or malformed" },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
} else if (!installationId) {
|
|
132
|
+
warnings.push("batch without an installation id");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sentAt = input.sentAt;
|
|
136
|
+
if (typeof sentAt !== "string" || !isIsoTimestamp(sentAt)) {
|
|
137
|
+
return { ok: false, error: { code: "invalid_sent_at", message: "sentAt is not a timestamp" } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const records = input.records;
|
|
141
|
+
if (!Array.isArray(records)) {
|
|
142
|
+
return { ok: false, error: { code: "invalid_records", message: "records must be an array" } };
|
|
143
|
+
}
|
|
144
|
+
if (records.length === 0) {
|
|
145
|
+
return { ok: false, error: { code: "empty_batch", message: "records must not be empty" } };
|
|
146
|
+
}
|
|
147
|
+
if (records.length > limits.maxRecords) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: {
|
|
151
|
+
code: "too_many_records",
|
|
152
|
+
message: `records exceed the limit of ${limits.maxRecords}`,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const sanitized: LogRecord[] = [];
|
|
158
|
+
for (const record of records) {
|
|
159
|
+
const result = sanitizeRecord(record, limits);
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
return { ok: false, error: result.error };
|
|
162
|
+
}
|
|
163
|
+
sanitized.push(result.record);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const batch: LogBatch = {
|
|
167
|
+
schemaVersion,
|
|
168
|
+
batchId,
|
|
169
|
+
app,
|
|
170
|
+
installationId: typeof installationId === "string" ? installationId : "legacy",
|
|
171
|
+
sentAt,
|
|
172
|
+
records: sanitized,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (typeof input.appVersion === "string") batch.appVersion = input.appVersion.slice(0, 32);
|
|
176
|
+
if (typeof input.hostLabel === "string") batch.hostLabel = input.hostLabel.slice(0, 128);
|
|
177
|
+
if (typeof input.sessionId === "string") batch.sessionId = input.sessionId.slice(0, 128);
|
|
178
|
+
|
|
179
|
+
return { ok: true, batch, warnings };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Record keys that are accepted. Everything else is dropped. */
|
|
183
|
+
const ALLOWED_RECORD_KEYS = new Set([
|
|
184
|
+
"ts",
|
|
185
|
+
"level",
|
|
186
|
+
"source",
|
|
187
|
+
"event",
|
|
188
|
+
"message",
|
|
189
|
+
"sessionId",
|
|
190
|
+
"durationMs",
|
|
191
|
+
"fields",
|
|
192
|
+
"error",
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Copy only the allowed properties of a record.
|
|
197
|
+
*
|
|
198
|
+
* This is the allowlist in action: a property the contract does not know is
|
|
199
|
+
* dropped before anything else happens to the record.
|
|
200
|
+
*/
|
|
201
|
+
function pickAllowedProperties(input: Record<string, unknown>): Record<string, unknown> {
|
|
202
|
+
const picked: Record<string, unknown> = {};
|
|
203
|
+
for (const key of ALLOWED_RECORD_KEYS) {
|
|
204
|
+
if (key in input) picked[key] = input[key];
|
|
205
|
+
}
|
|
206
|
+
return picked;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type SanitizeRecordResult =
|
|
210
|
+
| { ok: true; record: LogRecord }
|
|
211
|
+
| { ok: false; error: BatchValidationError };
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Rebuild one record from the allowlist and redact every string.
|
|
215
|
+
*
|
|
216
|
+
* Unknown properties are dropped, `error.name`, `error.message` and
|
|
217
|
+
* `error.stack` are redacted, and structured fields lose the keys that can
|
|
218
|
+
* carry private content.
|
|
219
|
+
*/
|
|
220
|
+
export function sanitizeRecord(
|
|
221
|
+
input: unknown,
|
|
222
|
+
limits: Partial<BatchValidationLimits> = {},
|
|
223
|
+
): SanitizeRecordResult {
|
|
224
|
+
const merged = { ...DEFAULT_BATCH_LIMITS, ...limits };
|
|
225
|
+
if (!isPlainObject(input)) {
|
|
226
|
+
return { ok: false, error: { code: "invalid_record", message: "record must be an object" } };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const allowed = pickAllowedProperties(input);
|
|
230
|
+
|
|
231
|
+
const ts = allowed.ts;
|
|
232
|
+
if (typeof ts !== "string" || !isIsoTimestamp(ts)) {
|
|
233
|
+
return { ok: false, error: { code: "invalid_ts", message: "ts is not a timestamp" } };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const level = allowed.level;
|
|
237
|
+
if (typeof level !== "string" || !LOG_LEVELS.includes(level as LogLevel)) {
|
|
238
|
+
return { ok: false, error: { code: "invalid_level", message: "level is not known" } };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const source = allowed.source;
|
|
242
|
+
if (typeof source !== "string" || source.length === 0 || source.length > 128) {
|
|
243
|
+
return { ok: false, error: { code: "invalid_source", message: "source is missing or malformed" } };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const event = allowed.event;
|
|
247
|
+
if (typeof event !== "string" || event.length === 0 || event.length > 128) {
|
|
248
|
+
return { ok: false, error: { code: "invalid_event", message: "event is missing or malformed" } };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const record: LogRecord = {
|
|
252
|
+
ts,
|
|
253
|
+
level: level as LogLevel,
|
|
254
|
+
source,
|
|
255
|
+
event,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (typeof allowed.message === "string") {
|
|
259
|
+
record.message = redactText(allowed.message).slice(0, merged.maxStringLength);
|
|
260
|
+
}
|
|
261
|
+
if (typeof allowed.sessionId === "string") {
|
|
262
|
+
record.sessionId = allowed.sessionId.slice(0, 128);
|
|
263
|
+
}
|
|
264
|
+
if (typeof allowed.durationMs === "number" && Number.isFinite(allowed.durationMs)) {
|
|
265
|
+
record.durationMs = allowed.durationMs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (isPlainObject(allowed.fields)) {
|
|
269
|
+
const fields: Record<string, unknown> = {};
|
|
270
|
+
let kept = 0;
|
|
271
|
+
for (const [key, value] of Object.entries(allowed.fields)) {
|
|
272
|
+
if (kept >= merged.maxFieldKeys) break;
|
|
273
|
+
if (isLocalOnlyFieldKey(key)) continue;
|
|
274
|
+
fields[key] = redactValue(value);
|
|
275
|
+
kept++;
|
|
276
|
+
}
|
|
277
|
+
if (Object.keys(fields).length > 0) record.fields = fields;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isPlainObject(allowed.error)) {
|
|
281
|
+
const error: LogErrorInfo = { name: "Error", message: "" };
|
|
282
|
+
if (typeof allowed.error.name === "string") {
|
|
283
|
+
error.name = redactText(allowed.error.name).slice(0, 256);
|
|
284
|
+
}
|
|
285
|
+
if (typeof allowed.error.message === "string") {
|
|
286
|
+
error.message = redactText(allowed.error.message).slice(0, merged.maxStringLength);
|
|
287
|
+
}
|
|
288
|
+
if (typeof allowed.error.stack === "string") {
|
|
289
|
+
error.stack = redactText(allowed.error.stack).slice(0, merged.maxStringLength);
|
|
290
|
+
}
|
|
291
|
+
record.error = error;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { ok: true, record };
|
|
295
|
+
}
|