@themoltnet/pi-extension 0.35.4 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +191 -0
  2. package/dist/index.js +6144 -323
  3. package/package.json +4 -3
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AgentSession } from '@earendil-works/pi-coding-agent';
2
2
  import { Api } from '@earendil-works/pi-ai';
3
3
  import { BashOperations } from '@earendil-works/pi-coding-agent';
4
+ import { CommandAnalysis } from '@themoltnet/shell-command-analyzer';
4
5
  import { connect } from '@themoltnet/sdk';
5
6
  import { EditOperations } from '@earendil-works/pi-coding-agent';
6
7
  import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
@@ -9,9 +10,11 @@ import { LoadSkillsResult } from '@earendil-works/pi-coding-agent';
9
10
  import { Model } from '@earendil-works/pi-ai';
10
11
  import { Readable } from 'node:stream';
11
12
  import { ReadOperations } from '@earendil-works/pi-coding-agent';
13
+ import { ShellCommandAnalyzer } from '@themoltnet/shell-command-analyzer';
12
14
  import { Skill } from '@earendil-works/pi-coding-agent';
13
15
  import { Static } from 'typebox';
14
16
  import { TObject } from 'typebox';
17
+ import { ToolCallEvent } from '@earendil-works/pi-coding-agent';
15
18
  import { ToolDefinition } from '@earendil-works/pi-coding-agent';
16
19
  import { TSchema } from 'typebox';
17
20
  import { Type } from 'typebox';
@@ -24,6 +27,18 @@ import { WriteOperations } from '@earendil-works/pi-coding-agent';
24
27
  */
25
28
  export declare function activateAgentEnv(agentEnv: Record<string, string | undefined>, repoRoot: string): void;
26
29
 
30
+ /** Minimal shape of the SDK method the resolver needs (keeps deps testable). */
31
+ export declare interface AllowedToolsClient {
32
+ runtimeProfiles: {
33
+ allowedTools: (profileId: string, options: {
34
+ teamId: string;
35
+ }) => Promise<{
36
+ enforcement: ToolEnforcement;
37
+ allowedTools: string[];
38
+ }>;
39
+ };
40
+ }
41
+
27
42
  /**
28
43
  * Construct an `AgentSession`. By default it is in-memory; callers may opt
29
44
  * parent sessions into daemon-owned file persistence via `sessionPersistence`.
@@ -63,6 +78,12 @@ declare interface BuildAgentSessionArgs {
63
78
  otelSpanAttrs: Record<string, string | number | boolean>;
64
79
  /** Agent name for `gen_ai.agent.name` on the root span. */
65
80
  agentName: string;
81
+ /**
82
+ * Extra pi extension factories appended after the always-on telemetry (and
83
+ * model-options) extensions — e.g. the tool-policy `tool_call` gate. Each is a
84
+ * plain `(pi) => void` registrar, same shape as the OTel extension.
85
+ */
86
+ extraExtensionFactories?: ((pi: ExtensionAPI) => void)[];
66
87
  /**
67
88
  * Parent sessions may persist their conversation history in a daemon-owned
68
89
  * directory. Subagents should leave this unset and stay in-memory.
@@ -260,8 +281,58 @@ export declare interface CreateSubagentToolArgs {
260
281
  * with whatever stubs they need.
261
282
  */
262
283
  contractRegistry: SubagentContractRegistry;
284
+ /**
285
+ * Extra pi extension factories every subagent session registers — chiefly the
286
+ * tool-policy `tool_call` gate. A subagent runs in its own `AgentSession`, so
287
+ * without re-registering the gate here it would execute tools un-checked,
288
+ * escaping the parent's enforcement (#1348 B2). Empty/undefined when tool
289
+ * enforcement is `off`, so subagents then match the parent's un-gated
290
+ * behaviour. The factories are read-only closures, safe to share across the
291
+ * parent and every subagent session.
292
+ */
293
+ extraExtensionFactories?: ((pi: ExtensionAPI) => void)[];
263
294
  }
264
295
 
296
+ /**
297
+ * A pi extension factory that gates every `tool_call` against the resolved
298
+ * policy. Blocks in `enforce`, audits (and allows) in `watch`, and is a no-op in
299
+ * `off`. Register it in a session's `extensionFactories`.
300
+ */
301
+ export declare function createToolPolicyExtension(deps: ToolPolicyExtensionDeps): (pi: ExtensionAPI) => void;
302
+
303
+ /**
304
+ * Map a pi `tool_call` event to a gate decision, extracting the shell command
305
+ * for `bash` and delegating to {@link decideToolCall}.
306
+ */
307
+ export declare function decideForEvent(event: ToolCallEvent, policy: SessionToolPolicy, analyze: ShellCommandAnalyzer['analyze']): GateDecision;
308
+
309
+ /**
310
+ * Decide whether a tool call is permitted by the resolved policy.
311
+ *
312
+ * Fail-closed in `enforce` (audited-but-allowed in `watch`, no-op in `off`) for:
313
+ *
314
+ * 1. **Unresolvable commands** — a `bash` command whose executables cannot be
315
+ * statically resolved (command substitution, `eval`, non-literal command
316
+ * names, unparseable input).
317
+ * 2. **Arbitrary-code interpreters** — a `bash` command that invokes a shell or
318
+ * language interpreter (`bash -c`, `python`, `node`, `perl`, …; the
319
+ * analyzer's `arbitrary-code` risk tier). Being name-listed is NOT enough:
320
+ * we cannot statically see the code such an interpreter runs, so the policy's
321
+ * allow-set can't bound it. This is the interim conservative stance for
322
+ * issue #1348 — an operator who lists `bash` still cannot smuggle
323
+ * `bash -c "curl … | sh"` past `enforce`.
324
+ * 3. **Unlisted executables** — any resolved executable not in `allowedTools`.
325
+ *
326
+ * KNOWN LIMITATION (follow-up): the `escapable` risk tier (GTFOBins binaries
327
+ * like `find`, `tar`, `awk` that document shell-spawn / file-write techniques)
328
+ * is NOT blocked on the tier alone. The analyzer already re-analyzes the
329
+ * sub-commands it can see through documented escape flags (`find -exec`,
330
+ * `tar --to-command`, …), but techniques it cannot parse statically could still
331
+ * escape a name-based allow-set. Tightening `escapable` (e.g. an LLM judge or a
332
+ * capability-aware allow-set) is tracked as future work.
333
+ */
334
+ export declare function decideToolCall(input: GateInput): GateDecision;
335
+
265
336
  /**
266
337
  * Ensure a cached snapshot exists, building one if needed.
267
338
  * Returns the absolute path to the qcow2 checkpoint file.
@@ -324,6 +395,14 @@ export declare interface ExecutePiTaskOptions {
324
395
  * claim. Task entries override profile entries with the same slug.
325
396
  */
326
397
  runtimeProfileContext?: readonly ContextRef[];
398
+ /**
399
+ * Runtime profile id, used to resolve the tool-policy allow-set at session
400
+ * start. Required together with a non-`off` `toolEnforcement` for the
401
+ * `tool_call` gate to run.
402
+ */
403
+ runtimeProfileId?: string;
404
+ /** Tool-policy enforcement mode for the selected runtime profile. */
405
+ toolEnforcement?: ToolEnforcement;
327
406
  /**
328
407
  * Forwarded to `buildTaskUserPrompt` for per-type builders. Static
329
408
  * across tasks. Today no built-in builder needs per-task `extras` —
@@ -455,6 +534,13 @@ export declare interface ExecutePiTaskOptions {
455
534
  * contracts. See #1106.
456
535
  */
457
536
  subagentContractRegistry?: SubagentContractRegistry;
537
+ /**
538
+ * Structured logger for tool-policy resolution/gate events. The daemon passes
539
+ * its task-bound pino child so these lines carry taskId/attemptN and join the
540
+ * run's NDJSON stream. When omitted, tool-policy events fall back to raw
541
+ * NDJSON on stderr (single-process / test callers). See #1348.
542
+ */
543
+ toolPolicyLogger?: ToolPolicyLogger;
458
544
  }
459
545
 
460
546
  /**
@@ -463,6 +549,38 @@ export declare interface ExecutePiTaskOptions {
463
549
  */
464
550
  export declare function findMainWorktree(): string;
465
551
 
552
+ /**
553
+ * The gate's verdict:
554
+ * - `{ allow: true }` — let the tool run.
555
+ * - `{ allow: false, reason }` — block it (enforce mode).
556
+ * - `{ audit, ... }` — would-block, but proceed and record it (watch mode).
557
+ */
558
+ export declare type GateDecision = {
559
+ allow: true;
560
+ } | {
561
+ allow: false;
562
+ reason: string;
563
+ } | {
564
+ audit: string;
565
+ missing?: string[];
566
+ };
567
+
568
+ export declare interface GateInput {
569
+ /** Pi tool name (e.g. 'bash', 'read', 'write', or a custom tool id). */
570
+ toolName: string;
571
+ /** The shell command, when `toolName === 'bash'`. */
572
+ command?: string;
573
+ enforcement: ToolEnforcement;
574
+ /** Names the policy allows (structured tool names + shell executable names). */
575
+ allowedTools: ReadonlySet<string>;
576
+ /**
577
+ * Synchronous shell analyzer (`ShellCommandAnalyzer.analyze`). Injected so the
578
+ * decision stays pure and testable; the analyzer's async WASM init happens
579
+ * once at session start.
580
+ */
581
+ analyze: (command: string) => CommandAnalysis;
582
+ }
583
+
466
584
  /**
467
585
  * Baseline env keys forwarded to host-exec child processes.
468
586
  * Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
@@ -732,6 +850,42 @@ export declare interface ProviderErrorRetryUi {
732
850
 
733
851
  export declare function redactRetryTriageSecrets(value: string): string;
734
852
 
853
+ /**
854
+ * Resolve the session's tool policy at start-up.
855
+ *
856
+ * `off` short-circuits without a network call. Otherwise the allowed-tool set
857
+ * is fetched from the API. If that fetch fails, the mode decides the fallback:
858
+ * `enforce` **fails closed** (empty allow-set → every non-`off` tool is
859
+ * blocked); `watch` fails open (empty allow-set → every tool is audited but
860
+ * allowed).
861
+ *
862
+ * The result is a **session-start snapshot**: it is resolved once and cached for
863
+ * the session's lifetime. Policy edits made while a task is running do not take
864
+ * effect until the next session — a deliberate trade-off (one resolution per
865
+ * session, stable enforcement for the run) accepted over re-fetching per call.
866
+ */
867
+ export declare function resolveSessionToolPolicy(input: ResolveSessionToolPolicyInput): Promise<SessionToolPolicy>;
868
+
869
+ declare interface ResolveSessionToolPolicyInput {
870
+ agent: AllowedToolsClient;
871
+ profileId: string;
872
+ teamId: string;
873
+ /**
874
+ * The profile's enforcement mode, already known to the daemon from the
875
+ * resolved runtime profile. Used to decide fail-open vs fail-closed when the
876
+ * allowed-tools fetch fails.
877
+ */
878
+ enforcement: ToolEnforcement;
879
+ logger: ToolPolicyLogger;
880
+ /**
881
+ * Deadline for the allowed-tools fetch. A hung API call must not stall session
882
+ * start-up indefinitely, so on timeout we abort and fall back to the
883
+ * mode-appropriate degraded policy. Defaults to
884
+ * {@link DEFAULT_RESOLVE_TIMEOUT_MS}; `0`/negative disables the deadline.
885
+ */
886
+ timeoutMs?: number;
887
+ }
888
+
735
889
  export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
736
890
 
737
891
  export declare interface ResumeCommand {
@@ -836,6 +990,22 @@ export declare interface SandboxConfig {
836
990
  };
837
991
  }
838
992
 
993
+ /** The resolved allow-set + enforcement mode for a runtime session. */
994
+ export declare interface SessionToolPolicy {
995
+ enforcement: ToolEnforcement;
996
+ allowedTools: ReadonlySet<string>;
997
+ /**
998
+ * `true` when the allow-set is a **degraded fallback** — the allowed-tools
999
+ * fetch failed or timed out and this policy is the fail-closed/fail-open
1000
+ * default, NOT the operator's actual configuration. An intentional
1001
+ * empty-but-resolved policy (e.g. a profile with no bound tools) has
1002
+ * `degraded: false`. Surfaced in every audit/block log so an operator can tell
1003
+ * "blocked because the policy is empty" from "blocked because we couldn't read
1004
+ * the policy". `off` and successful resolutions are never degraded.
1005
+ */
1006
+ degraded: boolean;
1007
+ }
1008
+
839
1009
  /** Extract snapshot-specific config for backwards compat with ensureSnapshot. */
840
1010
  export declare type SnapshotConfig = NonNullable<SandboxConfig['snapshot']>;
841
1011
 
@@ -1086,6 +1256,27 @@ declare type TaskUsage = Static<typeof TaskUsage>;
1086
1256
  */
1087
1257
  export declare function toGuestPath(localCwd: string, localPath: string, guestWorkspace: string): string;
1088
1258
 
1259
+ /** Enforcement mode resolved for the session's runtime profile. */
1260
+ export declare type ToolEnforcement = 'off' | 'watch' | 'enforce';
1261
+
1262
+ export declare interface ToolPolicyExtensionDeps {
1263
+ policy: SessionToolPolicy;
1264
+ analyzer: ShellCommandAnalyzer;
1265
+ logger: ToolPolicyLogger;
1266
+ }
1267
+
1268
+ /**
1269
+ * Structured logger the resolver and gate emit to. Deliberately the pino
1270
+ * `(obj, msg)` shape so the daemon can pass a task-bound pino child directly —
1271
+ * every tool-policy line then carries the daemon's taskId/attemptN context and
1272
+ * lands in the same NDJSON stream as the rest of the run.
1273
+ */
1274
+ declare interface ToolPolicyLogger {
1275
+ debug: (obj: Record<string, unknown>, msg: string) => void;
1276
+ info: (obj: Record<string, unknown>, msg: string) => void;
1277
+ warn: (obj: Record<string, unknown>, msg: string) => void;
1278
+ }
1279
+
1089
1280
  declare interface TrackedError {
1090
1281
  toolName: string;
1091
1282
  toolCallId: string;