@senad-d/observme 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -30,6 +30,7 @@
30
30
  - Fixed session, workflow, agent, turn, LLM, tool, Bash, histogram, active-span, failure/recovery, and session-count telemetry accuracy.
31
31
  - Fixed lifecycle serialization, duplicate session replacement, bounded shutdown/flush behavior, backfill cancellation, parallel tool correlation, and bounded agent-tree state cleanup.
32
32
  - Hardened configuration validation, project-root path confinement, endpoint security, custom redaction patterns, tenant-salted hashing, and environment propagation.
33
+ - Hardened integration discovery and lifecycle mutations against malformed providers, unsafe or oversized runtime inputs, and duplicate active spawn/wait/join identifiers.
33
34
  - Restored trusted-project redacted LLM capture and OpenAI Responses-style prompt capture while keeping capture disabled and redaction enabled by default.
34
35
  - Remediated SonarQube maintainability and security findings across parsing, regexes, fallback handling, diagnostics, and default assignment.
35
36
  - Fixed npm/CI dependency installation, package contents, README command drift, Grafana/Loki schema handling, and dashboard provisioning/query validation.
package/README.md CHANGED
@@ -167,7 +167,9 @@ if (started?.ok) {
167
167
  }
168
168
  ```
169
169
 
170
- Use [`docs/extension-integration.md`](docs/extension-integration.md) for the complete lifecycle and API contract. The shipped [`examples/integrations/subagent-runner.ts`](examples/integrations/subagent-runner.ts) wraps a generic child transport, while [`docs/agent-subagent-observability-requirements.md`](docs/agent-subagent-observability-requirements.md) covers the larger orchestration design.
170
+ The discovery helper fails open when the event bus or a provider is malformed. API methods also reject unsafe/oversized requests and duplicate active lifecycle IDs without replacing existing telemetry state. Handle every discriminated failure result locally and keep orchestration functional.
171
+
172
+ Use [`docs/extension-integration.md`](docs/extension-integration.md) for the complete lifecycle, validation limits, and failure contract. The shipped [`examples/integrations/subagent-runner.ts`](examples/integrations/subagent-runner.ts) wraps a generic child transport, while [`docs/agent-subagent-observability-requirements.md`](docs/agent-subagent-observability-requirements.md) covers the larger orchestration design.
171
173
 
172
174
  ## Commands
173
175
 
@@ -35,7 +35,7 @@ const observme: ObservMeIntegrationApi | undefined = requestObservMeIntegration(
35
35
 
36
36
  ObservMe registers no global object and does not require another extension to import its internal telemetry session. The event bus is the runtime boundary; the package subpath provides the stable constants, types, and request helper.
37
37
 
38
- The API can be absent when ObservMe is not installed, disabled by package configuration, not loaded yet, or incompatible. A method returns `{ ok: false, reason: "session_unavailable" }` when ObservMe is loaded but no telemetry session is active. Orchestration must remain functional in both cases and may run the child without ObservMe correlation after reporting a bounded local warning.
38
+ The API can be absent when ObservMe is not installed, disabled by package configuration, not loaded yet, incompatible, or connected through a failing/malformed event-bus provider. A method returns `{ ok: false, reason: "session_unavailable" }` when ObservMe is loaded but no telemetry session is active. Orchestration must remain functional in both cases and may run the child without ObservMe correlation after reporting a bounded local warning.
39
39
 
40
40
  A package that cannot take a runtime dependency can implement the same synchronous request directly. Keep this channel and version stable:
41
41
 
@@ -118,9 +118,21 @@ Do not put raw tasks, prompts, command lines, environment values, child output,
118
118
  | `toolCallId` | Optional high-cardinality trace/log correlation when a tool initiated the spawn. |
119
119
  | `env` | Base child environment. ObservMe removes stale lineage/W3C keys and returns the replacement environment. |
120
120
 
121
+ Runtime callers are validated even when JavaScript bypasses the TypeScript types. Caller-provided lifecycle identifiers must match `[A-Za-z0-9._:-]{1,128}`. Commands and individual arguments are capped at 4096 characters, argument lists at 256 items, and environment objects at 4096 entries. Durations must be finite, non-negative milliseconds. Invalid or duplicate active operations return a failure without replacing an existing span.
122
+
121
123
  Completion and wait/join methods use bounded child states (`starting`, `active`, `completed`, `failed`, `cancelled`, `orphaned`), join states (`waiting`, `completed`, `failed`, `cancelled`, `timeout`, `unknown`), and wait reasons (`dependency`, `rate_limit`, `child_running`, `unknown`). `failurePropagated=false` on a completed join confirms that the parent recovered from a failed child.
122
124
 
123
- All mutation methods return a discriminated result. Handle `session_unavailable`, `spawn_not_found`, `wait_not_found`, `join_not_found`, and `operation_failed` without crashing Pi. Do not retry a completed lifecycle handle blindly; repeated completion can otherwise hide an orchestration-state bug.
125
+ All mutation methods return a discriminated result. Handle these reasons without crashing Pi:
126
+
127
+ | Reason | Meaning |
128
+ | --- | --- |
129
+ | `session_unavailable` | ObservMe is loaded but no telemetry session is active. |
130
+ | `invalid_request` | An identifier, enum, duration, command/argument field, or environment shape is invalid or oversized. |
131
+ | `spawn_already_exists` / `wait_already_exists` / `join_already_exists` | The requested lifecycle identifier is already active. Generate a unique identifier or finish the active operation; do not overwrite it. |
132
+ | `spawn_not_found` / `wait_not_found` / `join_not_found` | The lifecycle handle is absent or has already ended. |
133
+ | `operation_failed` | ObservMe could not safely complete the operation. |
134
+
135
+ Do not retry a completed lifecycle handle blindly; repeated completion can otherwise hide an orchestration-state bug.
124
136
 
125
137
  ## Propagation environment
126
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/observme",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "ObservMe: a Pi extension that instruments Pi agent sessions and exports OpenTelemetry traces, metrics, and logs to an external observability stack (OTel Collector, Grafana Tempo/Loki/Prometheus).",
6
6
  "license": "MIT",
@@ -10,6 +10,10 @@ export type ObservMeChildStatus = "starting" | "active" | "completed" | "failed"
10
10
  export type ObservMeJoinStatus = "completed" | "failed" | "cancelled" | "timeout" | "unknown" | "waiting";
11
11
  export type ObservMeIntegrationFailureReason =
12
12
  | "session_unavailable"
13
+ | "invalid_request"
14
+ | "spawn_already_exists"
15
+ | "wait_already_exists"
16
+ | "join_already_exists"
13
17
  | "spawn_not_found"
14
18
  | "wait_not_found"
15
19
  | "join_not_found"
@@ -118,17 +122,55 @@ interface IntegrationResponseHolder {
118
122
  }
119
123
 
120
124
  export function requestObservMeIntegration(host: ObservMeIntegrationHost): ObservMeIntegrationApi | undefined {
125
+ const events = resolveIntegrationEventBus(host);
126
+ if (!events) return undefined;
127
+
121
128
  const holder: IntegrationResponseHolder = {};
122
129
  const request: ObservMeIntegrationRequest = {
123
130
  supportedVersions: [OBSERVME_INTEGRATION_VERSION],
124
131
  respond: receiveObservMeIntegration.bind(undefined, holder),
125
132
  };
126
133
 
127
- host.events.emit(OBSERVME_INTEGRATION_CHANNEL, request);
134
+ try {
135
+ events.emit(OBSERVME_INTEGRATION_CHANNEL, request);
136
+ } catch {
137
+ return undefined;
138
+ }
128
139
  return holder.api;
129
140
  }
130
141
 
131
- function receiveObservMeIntegration(holder: IntegrationResponseHolder, api: ObservMeIntegrationApi): void {
132
- if (api.version !== OBSERVME_INTEGRATION_VERSION || holder.api) return;
133
- holder.api = api;
142
+ function resolveIntegrationEventBus(host: unknown): ObservMeIntegrationEventBus | undefined {
143
+ if (!host || typeof host !== "object") return undefined;
144
+ try {
145
+ const events = (host as Partial<ObservMeIntegrationHost>).events;
146
+ return events && typeof events.emit === "function" ? events : undefined;
147
+ } catch {
148
+ return undefined;
149
+ }
150
+ }
151
+
152
+ function receiveObservMeIntegration(holder: IntegrationResponseHolder, value: unknown): void {
153
+ if (holder.api || !isObservMeIntegrationApi(value)) return;
154
+ holder.api = value;
155
+ }
156
+
157
+ function isObservMeIntegrationApi(value: unknown): value is ObservMeIntegrationApi {
158
+ if (!value || typeof value !== "object") return false;
159
+
160
+ try {
161
+ const api = value as Partial<ObservMeIntegrationApi>;
162
+ return (
163
+ api.version === OBSERVME_INTEGRATION_VERSION &&
164
+ typeof api.getContext === "function" &&
165
+ typeof api.startSubagent === "function" &&
166
+ typeof api.completeSubagent === "function" &&
167
+ typeof api.failSubagent === "function" &&
168
+ typeof api.startWait === "function" &&
169
+ typeof api.endWait === "function" &&
170
+ typeof api.startJoin === "function" &&
171
+ typeof api.endJoin === "function"
172
+ );
173
+ } catch {
174
+ return false;
175
+ }
134
176
  }
@@ -33,12 +33,22 @@ interface IntegrationPiApi {
33
33
  readonly events?: IntegrationEventBus;
34
34
  }
35
35
 
36
+ const integrationIdentifierPattern = /^[A-Za-z0-9._:-]{1,128}$/u;
37
+ const maximumIntegrationCommandLength = 4096;
38
+ const maximumIntegrationArgumentCount = 256;
39
+ const maximumIntegrationArgumentLength = 4096;
40
+ const maximumIntegrationEnvironmentEntries = 4096;
41
+
36
42
  export function registerObservMeIntegration(pi: unknown, state: HandlerSessionState): (() => void) | undefined {
37
43
  const events = resolveIntegrationEventBus(pi);
38
44
  if (!events) return undefined;
39
45
 
40
46
  const api = new SessionBackedObservMeIntegrationApi(state);
41
- return events.on(OBSERVME_INTEGRATION_CHANNEL, api.handleRequest.bind(api));
47
+ try {
48
+ return events.on(OBSERVME_INTEGRATION_CHANNEL, api.handleRequest.bind(api));
49
+ } catch {
50
+ return undefined;
51
+ }
42
52
  }
43
53
 
44
54
  export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationApi {
@@ -50,8 +60,8 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
50
60
  }
51
61
 
52
62
  handleRequest(value: unknown): void {
53
- if (!isCompatibleIntegrationRequest(value)) return;
54
63
  try {
64
+ if (!isCompatibleIntegrationRequest(value)) return;
55
65
  value.respond(this);
56
66
  } catch {
57
67
  return;
@@ -82,8 +92,12 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
82
92
  startSubagent(options: ObservMeStartSubagentOptions = {}): ObservMeStartedSubagent | ObservMeIntegrationFailure {
83
93
  const session = this.#state.session;
84
94
  if (!session) return integrationFailure("session_unavailable");
85
-
86
95
  try {
96
+ if (!isValidStartSubagentOptions(options)) return integrationFailure("invalid_request");
97
+ if (options.spawnId && session.spans.activeSubagentSpawns.has(options.spawnId)) {
98
+ return integrationFailure("spawn_already_exists");
99
+ }
100
+
87
101
  const started = startSubagentSpawn(session, options);
88
102
  return {
89
103
  ok: true,
@@ -103,9 +117,12 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
103
117
  ): ObservMeIntegrationSuccess | ObservMeIntegrationFailure {
104
118
  const session = this.#state.session;
105
119
  if (!session) return integrationFailure("session_unavailable");
106
- if (!session.spans.activeSubagentSpawns.get(spawnId)) return integrationFailure("spawn_not_found");
107
-
108
120
  try {
121
+ if (!isValidIntegrationIdentifier(spawnId) || !isValidCompleteSubagentOptions(options)) {
122
+ return integrationFailure("invalid_request");
123
+ }
124
+ if (!session.spans.activeSubagentSpawns.has(spawnId)) return integrationFailure("spawn_not_found");
125
+
109
126
  completeSubagentSpawn(session, spawnId, options);
110
127
  return integrationSuccess();
111
128
  } catch {
@@ -119,9 +136,12 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
119
136
  ): ObservMeIntegrationSuccess | ObservMeIntegrationFailure {
120
137
  const session = this.#state.session;
121
138
  if (!session) return integrationFailure("session_unavailable");
122
- if (!session.spans.activeSubagentSpawns.get(spawnId)) return integrationFailure("spawn_not_found");
123
-
124
139
  try {
140
+ if (!isValidIntegrationIdentifier(spawnId) || !isValidFailSubagentOptions(options)) {
141
+ return integrationFailure("invalid_request");
142
+ }
143
+ if (!session.spans.activeSubagentSpawns.has(spawnId)) return integrationFailure("spawn_not_found");
144
+
125
145
  failSubagentSpawn(session, spawnId, options);
126
146
  return integrationSuccess();
127
147
  } catch {
@@ -157,8 +177,15 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
157
177
  ): ObservMeStartedWaitJoin | ObservMeIntegrationFailure {
158
178
  const session = this.#state.session;
159
179
  if (!session) return integrationFailure("session_unavailable");
160
-
161
180
  try {
181
+ if (!isValidWaitJoinOptions(options)) return integrationFailure("invalid_request");
182
+
183
+ const requestedId = resolveRequestedWaitJoinId(options, kind);
184
+ const registry = kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
185
+ if (requestedId && registry.has(requestedId)) {
186
+ return integrationFailure(kind === "wait" ? "wait_already_exists" : "join_already_exists");
187
+ }
188
+
162
189
  const started = kind === "wait" ? startAgentWait(session, options) : startAgentJoin(session, options);
163
190
  return { ok: true, id: started.id };
164
191
  } catch {
@@ -173,10 +200,13 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
173
200
  ): ObservMeIntegrationSuccess | ObservMeIntegrationFailure {
174
201
  const session = this.#state.session;
175
202
  if (!session) return integrationFailure("session_unavailable");
176
- const registry = kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
177
- if (!registry.get(id)) return integrationFailure(kind === "wait" ? "wait_not_found" : "join_not_found");
178
-
179
203
  try {
204
+ if (!isValidIntegrationIdentifier(id) || !isValidWaitJoinOptions(options)) {
205
+ return integrationFailure("invalid_request");
206
+ }
207
+ const registry = kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
208
+ if (!registry.has(id)) return integrationFailure(kind === "wait" ? "wait_not_found" : "join_not_found");
209
+
180
210
  if (kind === "wait") endAgentWait(session, id, options);
181
211
  else endAgentJoin(session, id, options);
182
212
  return integrationSuccess();
@@ -188,8 +218,12 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
188
218
 
189
219
  function resolveIntegrationEventBus(pi: unknown): IntegrationEventBus | undefined {
190
220
  if (!pi || typeof pi !== "object") return undefined;
191
- const events = (pi as IntegrationPiApi).events;
192
- return events && typeof events.on === "function" ? events : undefined;
221
+ try {
222
+ const events = (pi as IntegrationPiApi).events;
223
+ return events && typeof events.on === "function" ? events : undefined;
224
+ } catch {
225
+ return undefined;
226
+ }
193
227
  }
194
228
 
195
229
  function isCompatibleIntegrationRequest(value: unknown): value is ObservMeIntegrationRequest {
@@ -202,6 +236,143 @@ function isCompatibleIntegrationRequest(value: unknown): value is ObservMeIntegr
202
236
  );
203
237
  }
204
238
 
239
+ function isValidStartSubagentOptions(value: unknown): value is ObservMeStartSubagentOptions {
240
+ if (!isIntegrationRecord(value)) return false;
241
+ const options = value as Partial<ObservMeStartSubagentOptions>;
242
+ return (
243
+ isOptionalIntegrationIdentifier(options.spawnId) &&
244
+ isOptionalIntegrationIdentifier(options.childAgentId) &&
245
+ isOptionalBoundedString(options.command, maximumIntegrationCommandLength) &&
246
+ isValidIntegrationArguments(options.args) &&
247
+ isOptionalSpawnType(options.spawnType) &&
248
+ isOptionalSpawnReason(options.spawnReason) &&
249
+ isOptionalIntegrationIdentifier(options.toolCallId) &&
250
+ isValidIntegrationEnvironment(options.env)
251
+ );
252
+ }
253
+
254
+ function isValidCompleteSubagentOptions(value: unknown): value is ObservMeCompleteSubagentOptions {
255
+ if (!isIntegrationRecord(value)) return false;
256
+ const options = value as Partial<ObservMeCompleteSubagentOptions>;
257
+ return (
258
+ isOptionalIntegrationIdentifier(options.childAgentId) &&
259
+ isOptionalChildStatus(options.childStatus) &&
260
+ (options.outcome === undefined || options.outcome === "completed" || options.outcome === "failed" || options.outcome === "cancelled")
261
+ );
262
+ }
263
+
264
+ function isValidFailSubagentOptions(value: unknown): value is ObservMeFailSubagentOptions {
265
+ if (!isIntegrationRecord(value)) return false;
266
+ const options = value as Partial<ObservMeFailSubagentOptions>;
267
+ return isOptionalIntegrationIdentifier(options.childAgentId) && isOptionalBoundedString(options.errorClass, 256);
268
+ }
269
+
270
+ function isValidWaitJoinOptions(value: unknown): value is ObservMeWaitJoinOptions {
271
+ if (!isIntegrationRecord(value)) return false;
272
+ const options = value as Partial<ObservMeWaitJoinOptions>;
273
+ return (
274
+ isOptionalIntegrationIdentifier(options.id) &&
275
+ isOptionalIntegrationIdentifier(options.spawnId) &&
276
+ isOptionalIntegrationIdentifier(options.childAgentId) &&
277
+ isOptionalChildStatus(options.childStatus) &&
278
+ isOptionalJoinStatus(options.joinStatus) &&
279
+ isOptionalWaitReason(options.reason) &&
280
+ (options.failurePropagated === undefined || typeof options.failurePropagated === "boolean") &&
281
+ (options.durationMs === undefined || isValidDuration(options.durationMs))
282
+ );
283
+ }
284
+
285
+ function isIntegrationRecord(value: unknown): value is Record<string, unknown> {
286
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
287
+ }
288
+
289
+ function isValidIntegrationIdentifier(value: unknown): value is string {
290
+ return typeof value === "string" && integrationIdentifierPattern.test(value);
291
+ }
292
+
293
+ function isOptionalIntegrationIdentifier(value: unknown): value is string | undefined {
294
+ return value === undefined || isValidIntegrationIdentifier(value);
295
+ }
296
+
297
+ function isOptionalBoundedString(value: unknown, maximumLength: number): value is string | undefined {
298
+ return value === undefined || (typeof value === "string" && value.length <= maximumLength);
299
+ }
300
+
301
+ function isValidIntegrationArguments(value: unknown): value is readonly string[] | undefined {
302
+ return (
303
+ value === undefined ||
304
+ (Array.isArray(value) &&
305
+ value.length <= maximumIntegrationArgumentCount &&
306
+ value.every(isValidIntegrationArgument))
307
+ );
308
+ }
309
+
310
+ function isValidIntegrationArgument(value: unknown): value is string {
311
+ return typeof value === "string" && value.length <= maximumIntegrationArgumentLength;
312
+ }
313
+
314
+ function isValidIntegrationEnvironment(value: unknown): boolean {
315
+ if (value === undefined) return true;
316
+ if (!isIntegrationRecord(value)) return false;
317
+ const entries = Object.entries(value);
318
+ return entries.length <= maximumIntegrationEnvironmentEntries && entries.every(isValidIntegrationEnvironmentEntry);
319
+ }
320
+
321
+ function isValidIntegrationEnvironmentEntry(entry: [string, unknown]): boolean {
322
+ return entry[0].length > 0 && (typeof entry[1] === "string" || entry[1] === undefined);
323
+ }
324
+
325
+ function isOptionalSpawnType(value: unknown): boolean {
326
+ return value === undefined || value === "command" || value === "tool" || value === "extension" || value === "unknown";
327
+ }
328
+
329
+ function isOptionalSpawnReason(value: unknown): boolean {
330
+ return (
331
+ value === undefined ||
332
+ value === "delegated_task" ||
333
+ value === "parallel_search" ||
334
+ value === "review" ||
335
+ value === "tool_wrapper" ||
336
+ value === "unknown"
337
+ );
338
+ }
339
+
340
+ function isOptionalChildStatus(value: unknown): boolean {
341
+ return (
342
+ value === undefined ||
343
+ value === "starting" ||
344
+ value === "active" ||
345
+ value === "completed" ||
346
+ value === "failed" ||
347
+ value === "cancelled" ||
348
+ value === "orphaned"
349
+ );
350
+ }
351
+
352
+ function isOptionalJoinStatus(value: unknown): boolean {
353
+ return (
354
+ value === undefined ||
355
+ value === "completed" ||
356
+ value === "failed" ||
357
+ value === "cancelled" ||
358
+ value === "timeout" ||
359
+ value === "unknown" ||
360
+ value === "waiting"
361
+ );
362
+ }
363
+
364
+ function isOptionalWaitReason(value: unknown): boolean {
365
+ return value === undefined || value === "dependency" || value === "rate_limit" || value === "child_running" || value === "unknown";
366
+ }
367
+
368
+ function isValidDuration(value: unknown): value is number {
369
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
370
+ }
371
+
372
+ function resolveRequestedWaitJoinId(options: ObservMeWaitJoinOptions, kind: "wait" | "join"): string | undefined {
373
+ return options.id ?? (options.spawnId ? `${kind}-${options.spawnId}` : undefined);
374
+ }
375
+
205
376
  function readSessionId(session: ObservMeTelemetrySession): string | undefined {
206
377
  const value = session.sessionAttributes?.[SESSION_ATTRIBUTES.PI_SESSION_ID];
207
378
  return typeof value === "string" ? value : undefined;