@agentic-kit/pi 0.26.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/embed/lanes.d.ts CHANGED
@@ -11,6 +11,7 @@ import type { MeteredIdentity } from '@agentic-kit/metering';
11
11
  import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
12
12
  import { type GateExtension, type GateExtensionOptions } from '../extensions/gate';
13
13
  import { type MeteredModelExtension, type MeteredModelExtensionOptions } from '../extensions/metered-model';
14
+ import { type PermissionsExtension, type PermissionsExtensionOptions } from '../extensions/permissions';
14
15
  import { type RunLogExtension, type RunLogExtensionOptions } from '../extensions/run-log';
15
16
  import { type UsageReportExtension, type UsageReportExtensionOptions } from '../extensions/usage-report';
16
17
  /** Run log lane — `runId` comes from the run, not from here. */
@@ -28,10 +29,17 @@ export type MeteringLane = ({
28
29
  } & UsageReportExtensionOptions);
29
30
  /** Approval lane — `runId` comes from the run. */
30
31
  export type GateLane = Omit<GateExtensionOptions, 'runId'>;
32
+ /**
33
+ * Permissions lane — the built-in tools (read, write, edit, bash, grep, find,
34
+ * ls) and secret redaction of every tool result. `runId` comes from the run.
35
+ */
36
+ export type PermissionsLane = Omit<PermissionsExtensionOptions, 'runId'>;
31
37
  export interface ComposeRunOptions {
32
38
  runId: string;
33
39
  log?: RunLogLane;
34
40
  metering?: MeteringLane;
41
+ /** Loaded before `gate`: it owns the built-in tools, the gate owns the rest. */
42
+ permissions?: PermissionsLane;
35
43
  gate?: GateLane;
36
44
  /**
37
45
  * The host's own extensions — workspace tools, prompts, UI glue. Loaded after
@@ -43,11 +51,12 @@ export interface ComposedLanes {
43
51
  log?: RunLogExtension;
44
52
  meteredModel?: MeteredModelExtension;
45
53
  usageReport?: UsageReportExtension;
54
+ permissions?: PermissionsExtension;
46
55
  gate?: GateExtension;
47
56
  }
48
57
  export interface ComposedRun {
49
58
  runId: string;
50
- /** In load order: log, metering, gate, then the host's own. */
59
+ /** In load order: log, metering, permissions, gate, then the host's own. */
51
60
  extensions: ExtensionFactory[];
52
61
  lanes: ComposedLanes;
53
62
  /**
package/embed/lanes.js CHANGED
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.composeRun = composeRun;
13
13
  const gate_1 = require("../extensions/gate");
14
14
  const metered_model_1 = require("../extensions/metered-model");
15
+ const permissions_1 = require("../extensions/permissions");
15
16
  const run_log_1 = require("../extensions/run-log");
16
17
  const usage_report_1 = require("../extensions/usage-report");
17
18
  function composeRun(options) {
@@ -40,6 +41,10 @@ function composeRun(options) {
40
41
  });
41
42
  extensions.push(lanes.usageReport.extension);
42
43
  }
44
+ if (options.permissions) {
45
+ lanes.permissions = (0, permissions_1.createPermissionsExtension)({ runId: options.runId, ...options.permissions });
46
+ extensions.push(lanes.permissions.extension);
47
+ }
43
48
  if (options.gate) {
44
49
  lanes.gate = (0, gate_1.createGateExtension)({ runId: options.runId, ...options.gate });
45
50
  extensions.push(lanes.gate.extension);
@@ -11,6 +11,7 @@ import type { MeteredIdentity } from '@agentic-kit/metering';
11
11
  import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
12
12
  import { type GateExtension, type GateExtensionOptions } from '../extensions/gate';
13
13
  import { type MeteredModelExtension, type MeteredModelExtensionOptions } from '../extensions/metered-model';
14
+ import { type PermissionsExtension, type PermissionsExtensionOptions } from '../extensions/permissions';
14
15
  import { type RunLogExtension, type RunLogExtensionOptions } from '../extensions/run-log';
15
16
  import { type UsageReportExtension, type UsageReportExtensionOptions } from '../extensions/usage-report';
16
17
  /** Run log lane — `runId` comes from the run, not from here. */
@@ -28,10 +29,17 @@ export type MeteringLane = ({
28
29
  } & UsageReportExtensionOptions);
29
30
  /** Approval lane — `runId` comes from the run. */
30
31
  export type GateLane = Omit<GateExtensionOptions, 'runId'>;
32
+ /**
33
+ * Permissions lane — the built-in tools (read, write, edit, bash, grep, find,
34
+ * ls) and secret redaction of every tool result. `runId` comes from the run.
35
+ */
36
+ export type PermissionsLane = Omit<PermissionsExtensionOptions, 'runId'>;
31
37
  export interface ComposeRunOptions {
32
38
  runId: string;
33
39
  log?: RunLogLane;
34
40
  metering?: MeteringLane;
41
+ /** Loaded before `gate`: it owns the built-in tools, the gate owns the rest. */
42
+ permissions?: PermissionsLane;
35
43
  gate?: GateLane;
36
44
  /**
37
45
  * The host's own extensions — workspace tools, prompts, UI glue. Loaded after
@@ -43,11 +51,12 @@ export interface ComposedLanes {
43
51
  log?: RunLogExtension;
44
52
  meteredModel?: MeteredModelExtension;
45
53
  usageReport?: UsageReportExtension;
54
+ permissions?: PermissionsExtension;
46
55
  gate?: GateExtension;
47
56
  }
48
57
  export interface ComposedRun {
49
58
  runId: string;
50
- /** In load order: log, metering, gate, then the host's own. */
59
+ /** In load order: log, metering, permissions, gate, then the host's own. */
51
60
  extensions: ExtensionFactory[];
52
61
  lanes: ComposedLanes;
53
62
  /**
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { createGateExtension } from '../extensions/gate';
11
11
  import { createMeteredModelExtension } from '../extensions/metered-model';
12
+ import { createPermissionsExtension } from '../extensions/permissions';
12
13
  import { createRunLogExtension } from '../extensions/run-log';
13
14
  import { createUsageReportExtension } from '../extensions/usage-report';
14
15
  export function composeRun(options) {
@@ -37,6 +38,10 @@ export function composeRun(options) {
37
38
  });
38
39
  extensions.push(lanes.usageReport.extension);
39
40
  }
41
+ if (options.permissions) {
42
+ lanes.permissions = createPermissionsExtension({ runId: options.runId, ...options.permissions });
43
+ extensions.push(lanes.permissions.extension);
44
+ }
40
45
  if (options.gate) {
41
46
  lanes.gate = createGateExtension({ runId: options.runId, ...options.gate });
42
47
  extensions.push(lanes.gate.extension);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The pi side of the permissions layer: classify every built-in tool call with
3
+ * the neutral engine, ask through the run's approval channel when it must, and
4
+ * keep secret values out of every tool result the model sees.
5
+ *
6
+ * Loaded by `composeRun` before the DB gate, so it decides on the input after
7
+ * every file-based extension ran and before trusted host code. It abstains on
8
+ * tools that are not pi built-ins: the DB gate and the host own those.
9
+ *
10
+ * Fails closed: an engine error blocks the call, and a redaction error
11
+ * withholds the output (pi swallows a `tool_result` throw and would send the
12
+ * raw output, so the catch here is the only guard).
13
+ */
14
+ import type { ApprovalChannel, ApprovalGrantScope } from '@agentic-kit/harness';
15
+ import { type Grant, type GrantStore, type PermissionDecision, type PermissionEngine, type PermissionMode, type Signal } from '@agentic-kit/permissions';
16
+ import type { createBashToolDefinition, ExtensionFactory } from '@earendil-works/pi-coding-agent';
17
+ /** The session entry that carries one chat-scope grant. Not sent to the model. */
18
+ export declare const PERMISSION_GRANT_ENTRY = "agentic.permissions.grant";
19
+ export declare const PERMISSION_INTERNAL_ERROR = "permissions: internal error \u2014 the call was blocked";
20
+ export declare const PERMISSION_OUTPUT_WITHHELD = "permissions: output withheld \u2014 redaction failed, so the result was not sent to the model";
21
+ export interface PermissionDecisionRecord {
22
+ runId: string;
23
+ toolCallId: string;
24
+ toolName: string;
25
+ /** What the engine said before any human answered. */
26
+ verdict: PermissionDecision['decision'];
27
+ /** How the call was finally settled. */
28
+ decision: 'allow' | 'deny';
29
+ reason?: string;
30
+ signals: Signal[];
31
+ targets: {
32
+ path: string;
33
+ access: string;
34
+ zone: string;
35
+ }[];
36
+ /** On an approved ask: how far the answer reaches. */
37
+ scope?: 'once' | ApprovalGrantScope;
38
+ actorId?: string;
39
+ decidedAt: string;
40
+ }
41
+ export interface PermissionsExtensionOptions {
42
+ runId: string;
43
+ engine: PermissionEngine;
44
+ /** Read at every decision, so a mode change applies to the next tool call. */
45
+ getMode: () => PermissionMode | Promise<PermissionMode>;
46
+ /** Required for `ask`. Without one, an ask becomes a deny that tells the model why. */
47
+ approvals?: ApprovalChannel;
48
+ /** Where "always in this project" grants live (a file per project on desktop). */
49
+ projectGrants?: GrantStore;
50
+ /** Every settled decision, for the run log / audit trail. */
51
+ onDecision?: (record: PermissionDecisionRecord) => void;
52
+ /** Internal failures (the call is already blocked or withheld). */
53
+ onError?: (error: unknown, phase: 'tool_call' | 'tool_result' | 'session_start') => void;
54
+ /**
55
+ * Replace pi's `bash` with one whose commands run without the secret-looking
56
+ * variables of the host environment, so a command cannot send a provider key
57
+ * or token it never saw. The host passes pi's own factory (this package
58
+ * imports pi as types only).
59
+ */
60
+ bash?: {
61
+ cwd: string;
62
+ createDefinition: typeof createBashToolDefinition;
63
+ };
64
+ now?: () => Date;
65
+ }
66
+ export interface PermissionsExtension {
67
+ extension: ExtensionFactory;
68
+ /** The grants this chat holds (restored from the session on start). */
69
+ readonly chatGrants: readonly Grant[];
70
+ }
71
+ export declare function createPermissionsExtension(options: PermissionsExtensionOptions): PermissionsExtension;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The pi side of the permissions layer: classify every built-in tool call with
3
+ * the neutral engine, ask through the run's approval channel when it must, and
4
+ * keep secret values out of every tool result the model sees.
5
+ *
6
+ * Loaded by `composeRun` before the DB gate, so it decides on the input after
7
+ * every file-based extension ran and before trusted host code. It abstains on
8
+ * tools that are not pi built-ins: the DB gate and the host own those.
9
+ *
10
+ * Fails closed: an engine error blocks the call, and a redaction error
11
+ * withholds the output (pi swallows a `tool_result` throw and would send the
12
+ * raw output, so the catch here is the only guard).
13
+ */
14
+ import { BUILTIN_TOOLS, grantFrom, scrubEnv } from '@agentic-kit/permissions';
15
+ /** The session entry that carries one chat-scope grant. Not sent to the model. */
16
+ export const PERMISSION_GRANT_ENTRY = 'agentic.permissions.grant';
17
+ export const PERMISSION_INTERNAL_ERROR = 'permissions: internal error — the call was blocked';
18
+ export const PERMISSION_OUTPUT_WITHHELD = 'permissions: output withheld — redaction failed, so the result was not sent to the model';
19
+ export function createPermissionsExtension(options) {
20
+ const { engine, runId } = options;
21
+ const now = options.now ?? (() => new Date());
22
+ const chatGrants = [];
23
+ const record = (decision, settled, extra = {}) => {
24
+ options.onDecision?.({
25
+ runId,
26
+ toolCallId: decision.request.toolCallId,
27
+ toolName: decision.request.toolName,
28
+ verdict: decision.decision,
29
+ decision: settled,
30
+ ...(extra.reason === undefined ? {} : { reason: extra.reason }),
31
+ signals: decision.request.signals,
32
+ targets: decision.request.targets.map(({ path, access, zone }) => ({ path, access, zone })),
33
+ ...(extra.scope === undefined ? {} : { scope: extra.scope }),
34
+ ...(extra.actorId === undefined ? {} : { actorId: extra.actorId }),
35
+ decidedAt: now().toISOString()
36
+ });
37
+ };
38
+ const remember = async (pi, decision, scope) => {
39
+ const option = decision.grantOptions.find((candidate) => candidate.scope === scope);
40
+ if (!option)
41
+ return;
42
+ const grant = grantFrom(option);
43
+ if (scope === 'chat') {
44
+ chatGrants.push(grant);
45
+ pi.appendEntry(PERMISSION_GRANT_ENTRY, grant);
46
+ }
47
+ else {
48
+ await options.projectGrants?.add(grant);
49
+ }
50
+ };
51
+ const decide = async (pi, event) => {
52
+ const input = (event.input ?? {});
53
+ const call = { toolCallId: event.toolCallId, toolName: event.toolName, input };
54
+ const projectGrants = (await options.projectGrants?.list()) ?? [];
55
+ const decision = await engine.evaluate(call, {
56
+ mode: await options.getMode(),
57
+ grants: [...chatGrants, ...projectGrants]
58
+ });
59
+ if (!decision)
60
+ return undefined;
61
+ if (decision.decision === 'allow') {
62
+ engine.restoreInput(decision, input);
63
+ record(decision, 'allow');
64
+ return undefined;
65
+ }
66
+ if (decision.decision === 'deny') {
67
+ record(decision, 'deny', { reason: decision.reason });
68
+ return { block: true, reason: decision.reason };
69
+ }
70
+ if (!options.approvals) {
71
+ const reason = `${decision.reason} — this needs user approval, and no one can approve it here; ask the user to allow it`;
72
+ record(decision, 'deny', { reason });
73
+ return { block: true, reason };
74
+ }
75
+ const outcome = await options.approvals.request({
76
+ runId,
77
+ toolCallId: event.toolCallId,
78
+ toolName: event.toolName,
79
+ input,
80
+ reason: decision.reason,
81
+ requestedAt: now().toISOString(),
82
+ permission: decision.prompt
83
+ });
84
+ if (outcome.decision !== 'allow') {
85
+ const reason = outcome.reason ?? `permissions: the user denied this ${event.toolName} call`;
86
+ record(decision, 'deny', { reason, actorId: outcome.actorId });
87
+ return { block: true, reason };
88
+ }
89
+ const scope = outcome.grant?.scope;
90
+ if (scope)
91
+ await remember(pi, decision, scope);
92
+ engine.restoreInput(decision, input, { approved: true });
93
+ record(decision, 'allow', { scope: scope ?? 'once', actorId: outcome.actorId });
94
+ return undefined;
95
+ };
96
+ const extension = (pi) => {
97
+ if (options.bash) {
98
+ const { cwd, createDefinition } = options.bash;
99
+ pi.registerTool(createDefinition(cwd, { spawnHook: (spawn) => ({ ...spawn, env: scrubEnv(spawn.env) }) }));
100
+ }
101
+ pi.on('session_start', async (_event, ctx) => {
102
+ chatGrants.length = 0;
103
+ try {
104
+ for (const entry of ctx.sessionManager.getEntries()) {
105
+ if (entry.type === 'custom' && entry.customType === PERMISSION_GRANT_ENTRY && entry.data) {
106
+ chatGrants.push(entry.data);
107
+ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ options.onError?.(error, 'session_start');
112
+ }
113
+ });
114
+ pi.on('tool_call', async (event) => {
115
+ if (!BUILTIN_TOOLS.has(event.toolName))
116
+ return undefined;
117
+ try {
118
+ return await decide(pi, event);
119
+ }
120
+ catch (error) {
121
+ options.onError?.(error, 'tool_call');
122
+ return { block: true, reason: PERMISSION_INTERNAL_ERROR };
123
+ }
124
+ });
125
+ // Every tool's output passes through redaction, not only the built-ins: a
126
+ // host tool can echo a token as easily as `cat .env` can.
127
+ pi.on('tool_result', async (event) => {
128
+ try {
129
+ const redacted = await engine.redactResult({
130
+ toolName: event.toolName,
131
+ content: event.content ?? [],
132
+ details: event.details
133
+ });
134
+ if (!redacted.changed)
135
+ return undefined;
136
+ // Redaction keeps every block's shape and only rewrites the text in it.
137
+ return { content: redacted.content, details: redacted.details };
138
+ }
139
+ catch (error) {
140
+ options.onError?.(error, 'tool_result');
141
+ return {
142
+ content: [{ type: 'text', text: PERMISSION_OUTPUT_WITHHELD }],
143
+ details: undefined,
144
+ isError: true
145
+ };
146
+ }
147
+ });
148
+ };
149
+ return { extension, chatGrants };
150
+ }
package/esm/index.d.ts CHANGED
@@ -12,10 +12,11 @@ export declare const dbTools: ExtensionFactory;
12
12
  /** Configure the host and get the extension in one call. */
13
13
  export declare function createDbTools(host: ToolsHost): ExtensionFactory;
14
14
  export { type ConfirmGate, type ConfirmGateDeps, type ConfirmGateOptions, createConfirmGate, } from './confirm-gate';
15
- export { type ComposedLanes, type ComposedRun, composeRun, type ComposeRunOptions, type GateLane, type MeteringLane, type RunLogLane, } from './embed/lanes';
15
+ export { type ComposedLanes, type ComposedRun, composeRun, type ComposeRunOptions, type GateLane, type MeteringLane, type PermissionsLane, type RunLogLane, } from './embed/lanes';
16
16
  export { type CreateResourceLoader, type EmbeddedRun, type PiModule, type ResourceLoaderRequest, startRun, type StartRunOptions, } from './embed/session';
17
17
  export { createGateExtension, type GateExtension, type GateExtensionOptions, } from './extensions/gate';
18
18
  export { createMeteredModelExtension, type MeteredModelExtension, type MeteredModelExtensionOptions, piProviderConfig, } from './extensions/metered-model';
19
+ export { createPermissionsExtension, PERMISSION_GRANT_ENTRY, PERMISSION_INTERNAL_ERROR, PERMISSION_OUTPUT_WITHHELD, type PermissionDecisionRecord, type PermissionsExtension, type PermissionsExtensionOptions, } from './extensions/permissions';
19
20
  export { createRunLogExtension, MIRROR_EVENTS, type MirrorEvent, type RunLogExtension, type RunLogExtensionOptions, } from './extensions/run-log';
20
21
  export { createUsageReportExtension, type UsageReportExtension, type UsageReportExtensionOptions, } from './extensions/usage-report';
21
22
  export { PI_HARNESS_ID, piHarness, type PiHarnessRun } from './harness';
package/esm/index.js CHANGED
@@ -31,6 +31,7 @@ export { composeRun, } from './embed/lanes';
31
31
  export { startRun, } from './embed/session';
32
32
  export { createGateExtension, } from './extensions/gate';
33
33
  export { createMeteredModelExtension, piProviderConfig, } from './extensions/metered-model';
34
+ export { createPermissionsExtension, PERMISSION_GRANT_ENTRY, PERMISSION_INTERNAL_ERROR, PERMISSION_OUTPUT_WITHHELD, } from './extensions/permissions';
34
35
  export { createRunLogExtension, MIRROR_EVENTS, } from './extensions/run-log';
35
36
  export { createUsageReportExtension, } from './extensions/usage-report';
36
37
  export { PI_HARNESS_ID, piHarness } from './harness';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The pi side of the permissions layer: classify every built-in tool call with
3
+ * the neutral engine, ask through the run's approval channel when it must, and
4
+ * keep secret values out of every tool result the model sees.
5
+ *
6
+ * Loaded by `composeRun` before the DB gate, so it decides on the input after
7
+ * every file-based extension ran and before trusted host code. It abstains on
8
+ * tools that are not pi built-ins: the DB gate and the host own those.
9
+ *
10
+ * Fails closed: an engine error blocks the call, and a redaction error
11
+ * withholds the output (pi swallows a `tool_result` throw and would send the
12
+ * raw output, so the catch here is the only guard).
13
+ */
14
+ import type { ApprovalChannel, ApprovalGrantScope } from '@agentic-kit/harness';
15
+ import { type Grant, type GrantStore, type PermissionDecision, type PermissionEngine, type PermissionMode, type Signal } from '@agentic-kit/permissions';
16
+ import type { createBashToolDefinition, ExtensionFactory } from '@earendil-works/pi-coding-agent';
17
+ /** The session entry that carries one chat-scope grant. Not sent to the model. */
18
+ export declare const PERMISSION_GRANT_ENTRY = "agentic.permissions.grant";
19
+ export declare const PERMISSION_INTERNAL_ERROR = "permissions: internal error \u2014 the call was blocked";
20
+ export declare const PERMISSION_OUTPUT_WITHHELD = "permissions: output withheld \u2014 redaction failed, so the result was not sent to the model";
21
+ export interface PermissionDecisionRecord {
22
+ runId: string;
23
+ toolCallId: string;
24
+ toolName: string;
25
+ /** What the engine said before any human answered. */
26
+ verdict: PermissionDecision['decision'];
27
+ /** How the call was finally settled. */
28
+ decision: 'allow' | 'deny';
29
+ reason?: string;
30
+ signals: Signal[];
31
+ targets: {
32
+ path: string;
33
+ access: string;
34
+ zone: string;
35
+ }[];
36
+ /** On an approved ask: how far the answer reaches. */
37
+ scope?: 'once' | ApprovalGrantScope;
38
+ actorId?: string;
39
+ decidedAt: string;
40
+ }
41
+ export interface PermissionsExtensionOptions {
42
+ runId: string;
43
+ engine: PermissionEngine;
44
+ /** Read at every decision, so a mode change applies to the next tool call. */
45
+ getMode: () => PermissionMode | Promise<PermissionMode>;
46
+ /** Required for `ask`. Without one, an ask becomes a deny that tells the model why. */
47
+ approvals?: ApprovalChannel;
48
+ /** Where "always in this project" grants live (a file per project on desktop). */
49
+ projectGrants?: GrantStore;
50
+ /** Every settled decision, for the run log / audit trail. */
51
+ onDecision?: (record: PermissionDecisionRecord) => void;
52
+ /** Internal failures (the call is already blocked or withheld). */
53
+ onError?: (error: unknown, phase: 'tool_call' | 'tool_result' | 'session_start') => void;
54
+ /**
55
+ * Replace pi's `bash` with one whose commands run without the secret-looking
56
+ * variables of the host environment, so a command cannot send a provider key
57
+ * or token it never saw. The host passes pi's own factory (this package
58
+ * imports pi as types only).
59
+ */
60
+ bash?: {
61
+ cwd: string;
62
+ createDefinition: typeof createBashToolDefinition;
63
+ };
64
+ now?: () => Date;
65
+ }
66
+ export interface PermissionsExtension {
67
+ extension: ExtensionFactory;
68
+ /** The grants this chat holds (restored from the session on start). */
69
+ readonly chatGrants: readonly Grant[];
70
+ }
71
+ export declare function createPermissionsExtension(options: PermissionsExtensionOptions): PermissionsExtension;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * The pi side of the permissions layer: classify every built-in tool call with
4
+ * the neutral engine, ask through the run's approval channel when it must, and
5
+ * keep secret values out of every tool result the model sees.
6
+ *
7
+ * Loaded by `composeRun` before the DB gate, so it decides on the input after
8
+ * every file-based extension ran and before trusted host code. It abstains on
9
+ * tools that are not pi built-ins: the DB gate and the host own those.
10
+ *
11
+ * Fails closed: an engine error blocks the call, and a redaction error
12
+ * withholds the output (pi swallows a `tool_result` throw and would send the
13
+ * raw output, so the catch here is the only guard).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PERMISSION_OUTPUT_WITHHELD = exports.PERMISSION_INTERNAL_ERROR = exports.PERMISSION_GRANT_ENTRY = void 0;
17
+ exports.createPermissionsExtension = createPermissionsExtension;
18
+ const permissions_1 = require("@agentic-kit/permissions");
19
+ /** The session entry that carries one chat-scope grant. Not sent to the model. */
20
+ exports.PERMISSION_GRANT_ENTRY = 'agentic.permissions.grant';
21
+ exports.PERMISSION_INTERNAL_ERROR = 'permissions: internal error — the call was blocked';
22
+ exports.PERMISSION_OUTPUT_WITHHELD = 'permissions: output withheld — redaction failed, so the result was not sent to the model';
23
+ function createPermissionsExtension(options) {
24
+ const { engine, runId } = options;
25
+ const now = options.now ?? (() => new Date());
26
+ const chatGrants = [];
27
+ const record = (decision, settled, extra = {}) => {
28
+ options.onDecision?.({
29
+ runId,
30
+ toolCallId: decision.request.toolCallId,
31
+ toolName: decision.request.toolName,
32
+ verdict: decision.decision,
33
+ decision: settled,
34
+ ...(extra.reason === undefined ? {} : { reason: extra.reason }),
35
+ signals: decision.request.signals,
36
+ targets: decision.request.targets.map(({ path, access, zone }) => ({ path, access, zone })),
37
+ ...(extra.scope === undefined ? {} : { scope: extra.scope }),
38
+ ...(extra.actorId === undefined ? {} : { actorId: extra.actorId }),
39
+ decidedAt: now().toISOString()
40
+ });
41
+ };
42
+ const remember = async (pi, decision, scope) => {
43
+ const option = decision.grantOptions.find((candidate) => candidate.scope === scope);
44
+ if (!option)
45
+ return;
46
+ const grant = (0, permissions_1.grantFrom)(option);
47
+ if (scope === 'chat') {
48
+ chatGrants.push(grant);
49
+ pi.appendEntry(exports.PERMISSION_GRANT_ENTRY, grant);
50
+ }
51
+ else {
52
+ await options.projectGrants?.add(grant);
53
+ }
54
+ };
55
+ const decide = async (pi, event) => {
56
+ const input = (event.input ?? {});
57
+ const call = { toolCallId: event.toolCallId, toolName: event.toolName, input };
58
+ const projectGrants = (await options.projectGrants?.list()) ?? [];
59
+ const decision = await engine.evaluate(call, {
60
+ mode: await options.getMode(),
61
+ grants: [...chatGrants, ...projectGrants]
62
+ });
63
+ if (!decision)
64
+ return undefined;
65
+ if (decision.decision === 'allow') {
66
+ engine.restoreInput(decision, input);
67
+ record(decision, 'allow');
68
+ return undefined;
69
+ }
70
+ if (decision.decision === 'deny') {
71
+ record(decision, 'deny', { reason: decision.reason });
72
+ return { block: true, reason: decision.reason };
73
+ }
74
+ if (!options.approvals) {
75
+ const reason = `${decision.reason} — this needs user approval, and no one can approve it here; ask the user to allow it`;
76
+ record(decision, 'deny', { reason });
77
+ return { block: true, reason };
78
+ }
79
+ const outcome = await options.approvals.request({
80
+ runId,
81
+ toolCallId: event.toolCallId,
82
+ toolName: event.toolName,
83
+ input,
84
+ reason: decision.reason,
85
+ requestedAt: now().toISOString(),
86
+ permission: decision.prompt
87
+ });
88
+ if (outcome.decision !== 'allow') {
89
+ const reason = outcome.reason ?? `permissions: the user denied this ${event.toolName} call`;
90
+ record(decision, 'deny', { reason, actorId: outcome.actorId });
91
+ return { block: true, reason };
92
+ }
93
+ const scope = outcome.grant?.scope;
94
+ if (scope)
95
+ await remember(pi, decision, scope);
96
+ engine.restoreInput(decision, input, { approved: true });
97
+ record(decision, 'allow', { scope: scope ?? 'once', actorId: outcome.actorId });
98
+ return undefined;
99
+ };
100
+ const extension = (pi) => {
101
+ if (options.bash) {
102
+ const { cwd, createDefinition } = options.bash;
103
+ pi.registerTool(createDefinition(cwd, { spawnHook: (spawn) => ({ ...spawn, env: (0, permissions_1.scrubEnv)(spawn.env) }) }));
104
+ }
105
+ pi.on('session_start', async (_event, ctx) => {
106
+ chatGrants.length = 0;
107
+ try {
108
+ for (const entry of ctx.sessionManager.getEntries()) {
109
+ if (entry.type === 'custom' && entry.customType === exports.PERMISSION_GRANT_ENTRY && entry.data) {
110
+ chatGrants.push(entry.data);
111
+ }
112
+ }
113
+ }
114
+ catch (error) {
115
+ options.onError?.(error, 'session_start');
116
+ }
117
+ });
118
+ pi.on('tool_call', async (event) => {
119
+ if (!permissions_1.BUILTIN_TOOLS.has(event.toolName))
120
+ return undefined;
121
+ try {
122
+ return await decide(pi, event);
123
+ }
124
+ catch (error) {
125
+ options.onError?.(error, 'tool_call');
126
+ return { block: true, reason: exports.PERMISSION_INTERNAL_ERROR };
127
+ }
128
+ });
129
+ // Every tool's output passes through redaction, not only the built-ins: a
130
+ // host tool can echo a token as easily as `cat .env` can.
131
+ pi.on('tool_result', async (event) => {
132
+ try {
133
+ const redacted = await engine.redactResult({
134
+ toolName: event.toolName,
135
+ content: event.content ?? [],
136
+ details: event.details
137
+ });
138
+ if (!redacted.changed)
139
+ return undefined;
140
+ // Redaction keeps every block's shape and only rewrites the text in it.
141
+ return { content: redacted.content, details: redacted.details };
142
+ }
143
+ catch (error) {
144
+ options.onError?.(error, 'tool_result');
145
+ return {
146
+ content: [{ type: 'text', text: exports.PERMISSION_OUTPUT_WITHHELD }],
147
+ details: undefined,
148
+ isError: true
149
+ };
150
+ }
151
+ });
152
+ };
153
+ return { extension, chatGrants };
154
+ }
package/index.d.ts CHANGED
@@ -12,10 +12,11 @@ export declare const dbTools: ExtensionFactory;
12
12
  /** Configure the host and get the extension in one call. */
13
13
  export declare function createDbTools(host: ToolsHost): ExtensionFactory;
14
14
  export { type ConfirmGate, type ConfirmGateDeps, type ConfirmGateOptions, createConfirmGate, } from './confirm-gate';
15
- export { type ComposedLanes, type ComposedRun, composeRun, type ComposeRunOptions, type GateLane, type MeteringLane, type RunLogLane, } from './embed/lanes';
15
+ export { type ComposedLanes, type ComposedRun, composeRun, type ComposeRunOptions, type GateLane, type MeteringLane, type PermissionsLane, type RunLogLane, } from './embed/lanes';
16
16
  export { type CreateResourceLoader, type EmbeddedRun, type PiModule, type ResourceLoaderRequest, startRun, type StartRunOptions, } from './embed/session';
17
17
  export { createGateExtension, type GateExtension, type GateExtensionOptions, } from './extensions/gate';
18
18
  export { createMeteredModelExtension, type MeteredModelExtension, type MeteredModelExtensionOptions, piProviderConfig, } from './extensions/metered-model';
19
+ export { createPermissionsExtension, PERMISSION_GRANT_ENTRY, PERMISSION_INTERNAL_ERROR, PERMISSION_OUTPUT_WITHHELD, type PermissionDecisionRecord, type PermissionsExtension, type PermissionsExtensionOptions, } from './extensions/permissions';
19
20
  export { createRunLogExtension, MIRROR_EVENTS, type MirrorEvent, type RunLogExtension, type RunLogExtensionOptions, } from './extensions/run-log';
20
21
  export { createUsageReportExtension, type UsageReportExtension, type UsageReportExtensionOptions, } from './extensions/usage-report';
21
22
  export { PI_HARNESS_ID, piHarness, type PiHarnessRun } from './harness';
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getModulePreset = exports.allModulePresets = exports.toolSchema = exports.resolveProvisionModules = exports.resolveProjectContext = exports.resolveDataToken = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.moduleKey = exports.loadProvisionManifest = exports.getHost = exports.fromEnvironment = exports.fromEnvFile = exports.deriveSubdomainEndpoint = exports.DEFAULT_PROVISION_PRESET = exports.createTemplatePreviewTables = exports.CONTEXT_ENV_PREFIX = exports.CONTEXT_ENV_KEYS = exports.constructiveDbTools = exports.configureHost = exports.toPiTools = exports.toPiTool = exports.piHarness = exports.PI_HARNESS_ID = exports.createUsageReportExtension = exports.MIRROR_EVENTS = exports.createRunLogExtension = exports.piProviderConfig = exports.createMeteredModelExtension = exports.createGateExtension = exports.startRun = exports.composeRun = exports.createConfirmGate = exports.dbTools = void 0;
3
+ exports.getModulePreset = exports.allModulePresets = exports.toolSchema = exports.resolveProvisionModules = exports.resolveProjectContext = exports.resolveDataToken = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.moduleKey = exports.loadProvisionManifest = exports.getHost = exports.fromEnvironment = exports.fromEnvFile = exports.deriveSubdomainEndpoint = exports.DEFAULT_PROVISION_PRESET = exports.createTemplatePreviewTables = exports.CONTEXT_ENV_PREFIX = exports.CONTEXT_ENV_KEYS = exports.constructiveDbTools = exports.configureHost = exports.toPiTools = exports.toPiTool = exports.piHarness = exports.PI_HARNESS_ID = exports.createUsageReportExtension = exports.MIRROR_EVENTS = exports.createRunLogExtension = exports.PERMISSION_OUTPUT_WITHHELD = exports.PERMISSION_INTERNAL_ERROR = exports.PERMISSION_GRANT_ENTRY = exports.createPermissionsExtension = exports.piProviderConfig = exports.createMeteredModelExtension = exports.createGateExtension = exports.startRun = exports.composeRun = exports.createConfirmGate = exports.dbTools = void 0;
4
4
  exports.createDbTools = createDbTools;
5
5
  const db_tools_1 = require("@agentic-kit/db-tools");
6
6
  const confirm_gate_1 = require("./confirm-gate");
@@ -42,6 +42,11 @@ Object.defineProperty(exports, "createGateExtension", { enumerable: true, get: f
42
42
  var metered_model_1 = require("./extensions/metered-model");
43
43
  Object.defineProperty(exports, "createMeteredModelExtension", { enumerable: true, get: function () { return metered_model_1.createMeteredModelExtension; } });
44
44
  Object.defineProperty(exports, "piProviderConfig", { enumerable: true, get: function () { return metered_model_1.piProviderConfig; } });
45
+ var permissions_1 = require("./extensions/permissions");
46
+ Object.defineProperty(exports, "createPermissionsExtension", { enumerable: true, get: function () { return permissions_1.createPermissionsExtension; } });
47
+ Object.defineProperty(exports, "PERMISSION_GRANT_ENTRY", { enumerable: true, get: function () { return permissions_1.PERMISSION_GRANT_ENTRY; } });
48
+ Object.defineProperty(exports, "PERMISSION_INTERNAL_ERROR", { enumerable: true, get: function () { return permissions_1.PERMISSION_INTERNAL_ERROR; } });
49
+ Object.defineProperty(exports, "PERMISSION_OUTPUT_WITHHELD", { enumerable: true, get: function () { return permissions_1.PERMISSION_OUTPUT_WITHHELD; } });
45
50
  var run_log_1 = require("./extensions/run-log");
46
51
  Object.defineProperty(exports, "createRunLogExtension", { enumerable: true, get: function () { return run_log_1.createRunLogExtension; } });
47
52
  Object.defineProperty(exports, "MIRROR_EVENTS", { enumerable: true, get: function () { return run_log_1.MIRROR_EVENTS; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-kit/pi",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "the pi adapter for agentic-kit — Constructive's typed db tools, confirm gate, and the run-log/metering/gate lanes attached to a pi coding-agent session",
6
6
  "main": "index.js",
@@ -29,9 +29,10 @@
29
29
  "test:watch": "jest --watch"
30
30
  },
31
31
  "dependencies": {
32
- "@agentic-kit/db-tools": "^0.10.2",
33
- "@agentic-kit/harness": "^0.17.0",
32
+ "@agentic-kit/db-tools": "^0.10.3",
33
+ "@agentic-kit/harness": "^0.18.0",
34
34
  "@agentic-kit/metering": "^0.6.0",
35
+ "@agentic-kit/permissions": "^0.2.0",
35
36
  "@agentic-kit/run-log": "^0.8.0",
36
37
  "zod": "^4.4.3"
37
38
  },
@@ -47,5 +48,5 @@
47
48
  "coding-agent",
48
49
  "constructive"
49
50
  ],
50
- "gitHead": "91e18cc80da92331fb3aadd5baa43251c8934bc8"
51
+ "gitHead": "9d1255b0e72a134fbc96190b2e1b8b0b0155bc29"
51
52
  }