@ory/pi 0.10.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/README.md +129 -0
- package/dist/cli/assets.d.ts +14 -0
- package/dist/cli/assets.js +88 -0
- package/dist/cli/loader.d.ts +41 -0
- package/dist/cli/loader.js +104 -0
- package/dist/cli/main.d.ts +11 -0
- package/dist/cli/main.js +195 -0
- package/dist/cli/setup.d.ts +34 -0
- package/dist/cli/setup.js +98 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +11 -0
- package/dist/plugin.d.ts +42 -0
- package/dist/plugin.js +319 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +26 -0
- package/package.json +92 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Setup CLI for the Ory Pi plugin.
|
|
5
|
+
*
|
|
6
|
+
* Pi auto-discovers in-process extensions from `<cwd>/.pi/extensions/` (project
|
|
7
|
+
* scope) and `~/.pi/agent/extensions/` (global scope): every `*.ts`/`*.js` file
|
|
8
|
+
* placed there is loaded via jiti and its default export is invoked as the
|
|
9
|
+
* extension factory. A bare module id in `settings.json`'s `extensions` array is
|
|
10
|
+
* NOT loaded — those entries are resolved as filesystem paths.
|
|
11
|
+
*
|
|
12
|
+
* So this writer drops a tiny loader file that re-exports the installed
|
|
13
|
+
* `@ory/pi` package's default factory:
|
|
14
|
+
*
|
|
15
|
+
* // .pi/extensions/ory.js
|
|
16
|
+
* export { default } from "@ory/pi";
|
|
17
|
+
*
|
|
18
|
+
* Pi discovers the file, jiti resolves `@ory/pi` from `node_modules`, and the
|
|
19
|
+
* factory runs. Project scope writes `<projectDir>/.pi/extensions/ory.js`;
|
|
20
|
+
* global scope (`--global`) writes `~/.pi/agent/extensions/ory.js`. No
|
|
21
|
+
* `settings.json` entry is required — discovery alone enables the extension.
|
|
22
|
+
*
|
|
23
|
+
* Pi excludes MCP by design, so there is NO MCP server block.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* npx ory-pi-setup # Install loader into project .pi/extensions/
|
|
27
|
+
* npx ory-pi-setup --global # Install loader into ~/.pi/agent/extensions/
|
|
28
|
+
* npx ory-pi-setup --project-dir /path/to/repo
|
|
29
|
+
* npx ory-pi-setup --print # Print the loader contents to stdout
|
|
30
|
+
* npx ory-pi-setup --uninstall # Remove the Ory loader
|
|
31
|
+
*
|
|
32
|
+
* Verified against @earendil-works/pi-coding-agent v0.80.2 (the extension
|
|
33
|
+
* loader's `discoverAndLoadExtensions` / `discoverExtensionsInDir`).
|
|
34
|
+
*/
|
|
35
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
38
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
39
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
40
|
+
}
|
|
41
|
+
Object.defineProperty(o, k2, desc);
|
|
42
|
+
}) : (function(o, m, k, k2) {
|
|
43
|
+
if (k2 === undefined) k2 = k;
|
|
44
|
+
o[k2] = m[k];
|
|
45
|
+
}));
|
|
46
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
47
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
48
|
+
}) : function(o, v) {
|
|
49
|
+
o["default"] = v;
|
|
50
|
+
});
|
|
51
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
52
|
+
var ownKeys = function(o) {
|
|
53
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
54
|
+
var ar = [];
|
|
55
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
56
|
+
return ar;
|
|
57
|
+
};
|
|
58
|
+
return ownKeys(o);
|
|
59
|
+
};
|
|
60
|
+
return function (mod) {
|
|
61
|
+
if (mod && mod.__esModule) return mod;
|
|
62
|
+
var result = {};
|
|
63
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
64
|
+
__setModuleDefault(result, mod);
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
})();
|
|
68
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
|
+
const path = __importStar(require("node:path"));
|
|
70
|
+
const argus_1 = require("@ory/argus");
|
|
71
|
+
const assets_js_1 = require("./assets.js");
|
|
72
|
+
const loader_js_1 = require("./loader.js");
|
|
73
|
+
function main() {
|
|
74
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
75
|
+
(0, argus_1.printSetupHelp)("ory-pi-setup", "Pi");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
const args = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
|
|
79
|
+
if (args.print) {
|
|
80
|
+
console.log(loader_js_1.LOADER_CONTENTS);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (args.uninstall) {
|
|
84
|
+
const removed = (0, loader_js_1.removeOryLoader)(args);
|
|
85
|
+
console.log(removed
|
|
86
|
+
? "Removed Ory extension loader."
|
|
87
|
+
: "No Ory extension loader found. Nothing to uninstall.");
|
|
88
|
+
(0, assets_js_1.uninstallPiOryAssets)(args.projectDir);
|
|
89
|
+
console.log(`Removed Ory skills from ${path.join(args.projectDir, ".pi", "skills")}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const loaderPath = (0, loader_js_1.writeOryLoader)(args);
|
|
93
|
+
(0, assets_js_1.installPiOryAssets)(args.projectDir);
|
|
94
|
+
console.log(`Ory extension loader installed at ${loaderPath}`);
|
|
95
|
+
console.log(`Ory skills installed to ${path.join(args.projectDir, ".pi", "skills")}`);
|
|
96
|
+
(0, argus_1.printNextSteps)("Pi", "npx ory-pi-setup --uninstall");
|
|
97
|
+
}
|
|
98
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createOryPlugin } from "./plugin.js";
|
|
2
|
+
export { createOryPlugin };
|
|
3
|
+
export type { CreateOryPluginDeps } from "./plugin.js";
|
|
4
|
+
export type { PiExtension, PiExtensionApi, PiEvent, PiEventHandlers, PiToolCallContext, PiToolResultContext, PiToolCallResult, PiHookRuntimeContext, } from "./types.js";
|
|
5
|
+
declare const _default: import("./types.js").PiExtension;
|
|
6
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createOryPlugin = void 0;
|
|
4
|
+
const plugin_js_1 = require("./plugin.js");
|
|
5
|
+
Object.defineProperty(exports, "createOryPlugin", { enumerable: true, get: function () { return plugin_js_1.createOryPlugin; } });
|
|
6
|
+
// Pi loads in-process extensions via jiti and expects a default-exported
|
|
7
|
+
// factory function `(pi) => void | Promise<void>`. CJS output (module:
|
|
8
|
+
// nodenext → CJS) surfaces `module.exports` as the ESM `default`, so a single
|
|
9
|
+
// default export is what Pi's loader receives. Instantiate the plugin from env.
|
|
10
|
+
// eslint-disable-next-line import/no-default-export
|
|
11
|
+
exports.default = (0, plugin_js_1.createOryPlugin)();
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi in-process extension (auth + blocking permission gate + tracing).
|
|
3
|
+
*
|
|
4
|
+
* Pi loads a default-exported factory `(pi) => void | Promise<void>` from a
|
|
5
|
+
* TypeScript extension module (via jiti) and awaits it before startup
|
|
6
|
+
* continues. We treat the factory body as the session-start hook: it runs the
|
|
7
|
+
* user gate (advisory — there is no block channel for the session itself), the
|
|
8
|
+
* agent gate, and writes the user→agent delegation tuple.
|
|
9
|
+
*
|
|
10
|
+
* The blocking permission decision lives in the `tool_call` hook, which CAN
|
|
11
|
+
* block by returning `{ block: true, reason }` (like OpenClaw's
|
|
12
|
+
* `before_tool_call`). The `tool_result` hook records the `tool.complete`
|
|
13
|
+
* audit span after a tool runs.
|
|
14
|
+
*
|
|
15
|
+
* Pi excludes MCP and sub-agents by design, so there is NO MCP tool parsing
|
|
16
|
+
* and NO sub-agent identity path.
|
|
17
|
+
*
|
|
18
|
+
* Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
|
|
19
|
+
* core/extensions/types.d.ts and the extension loader): the `tool_call`
|
|
20
|
+
* handler returns `ToolCallEventResult { block?: boolean; reason? }`,
|
|
21
|
+
* `tool_result` carries `isError`/`content`, and Pi awaits the async factory
|
|
22
|
+
* default export.
|
|
23
|
+
*/
|
|
24
|
+
import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
|
|
25
|
+
import type { PiExtension } from "./types.js";
|
|
26
|
+
export interface CreateOryPluginDeps {
|
|
27
|
+
/** Test injection point for the user login flow. */
|
|
28
|
+
userLogin?: typeof ensureUserAuthenticated;
|
|
29
|
+
/** Test injection point for the agent identity gate. */
|
|
30
|
+
agentGate?: typeof ensureAgentIdentity;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create the Ory extension factory for Pi.
|
|
34
|
+
*
|
|
35
|
+
* Returns a function matching Pi's `PiExtension` contract: it receives the
|
|
36
|
+
* ExtensionAPI, runs the session-start auth gates in the body, and registers
|
|
37
|
+
* the `tool_call` (blocking) and `tool_result` (tracing) hooks.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
|
|
40
|
+
projectUrl: string;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
}, deps?: CreateOryPluginDeps): PiExtension;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pi in-process extension (auth + blocking permission gate + tracing).
|
|
4
|
+
*
|
|
5
|
+
* Pi loads a default-exported factory `(pi) => void | Promise<void>` from a
|
|
6
|
+
* TypeScript extension module (via jiti) and awaits it before startup
|
|
7
|
+
* continues. We treat the factory body as the session-start hook: it runs the
|
|
8
|
+
* user gate (advisory — there is no block channel for the session itself), the
|
|
9
|
+
* agent gate, and writes the user→agent delegation tuple.
|
|
10
|
+
*
|
|
11
|
+
* The blocking permission decision lives in the `tool_call` hook, which CAN
|
|
12
|
+
* block by returning `{ block: true, reason }` (like OpenClaw's
|
|
13
|
+
* `before_tool_call`). The `tool_result` hook records the `tool.complete`
|
|
14
|
+
* audit span after a tool runs.
|
|
15
|
+
*
|
|
16
|
+
* Pi excludes MCP and sub-agents by design, so there is NO MCP tool parsing
|
|
17
|
+
* and NO sub-agent identity path.
|
|
18
|
+
*
|
|
19
|
+
* Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
|
|
20
|
+
* core/extensions/types.d.ts and the extension loader): the `tool_call`
|
|
21
|
+
* handler returns `ToolCallEventResult { block?: boolean; reason? }`,
|
|
22
|
+
* `tool_result` carries `isError`/`content`, and Pi awaits the async factory
|
|
23
|
+
* default export.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createOryPlugin = createOryPlugin;
|
|
27
|
+
const argus_1 = require("@ory/argus");
|
|
28
|
+
/**
|
|
29
|
+
* Create the Ory extension factory for Pi.
|
|
30
|
+
*
|
|
31
|
+
* Returns a function matching Pi's `PiExtension` contract: it receives the
|
|
32
|
+
* ExtensionAPI, runs the session-start auth gates in the body, and registers
|
|
33
|
+
* the `tool_call` (blocking) and `tool_result` (tracing) hooks.
|
|
34
|
+
*/
|
|
35
|
+
function createOryPlugin(clientOrConfig, deps = {}) {
|
|
36
|
+
return async (pi) => {
|
|
37
|
+
const client = clientOrConfig instanceof argus_1.OryAgentClient
|
|
38
|
+
? clientOrConfig
|
|
39
|
+
: clientOrConfig
|
|
40
|
+
? new argus_1.OryAgentClient({ ...clientOrConfig, harness: "pi" })
|
|
41
|
+
: argus_1.OryAgentClient.fromEnv("pi");
|
|
42
|
+
// The factory body IS the session-start hook: Pi awaits async extension
|
|
43
|
+
// factories before startup continues (verified against
|
|
44
|
+
// @earendil-works/pi-coding-agent v0.80.2). Pi also exposes a dedicated
|
|
45
|
+
// `session_start` event, but the factory body is sufficient and runs
|
|
46
|
+
// exactly once per process.
|
|
47
|
+
await runSessionStart(client, deps);
|
|
48
|
+
pi.on("tool_call", createToolCallHandler(client));
|
|
49
|
+
pi.on("tool_result", createToolResultHandler(client));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ─── session start (factory body) ──────────────────────────────────
|
|
53
|
+
async function runSessionStart(client, deps) {
|
|
54
|
+
client.logger.info("lifecycle.session_start", { harness: "pi" });
|
|
55
|
+
client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
|
|
56
|
+
client.tracer.record("session.start", "ok", { attributes: {} });
|
|
57
|
+
// Run the user login. Pi's factory body has no return-value channel to
|
|
58
|
+
// block the session, so allowBlock is false — the flow emits the user.auth
|
|
59
|
+
// audit span, refreshes tokens, and may prompt when interactive, but never
|
|
60
|
+
// prevents the session from starting. When ORY_USER_LOGIN is unset it is a
|
|
61
|
+
// no-op (mode === "disabled") and we fall through to legacy verification.
|
|
62
|
+
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
63
|
+
const decision = await userGate(client, {
|
|
64
|
+
binName: "ory-pi",
|
|
65
|
+
harness: "pi",
|
|
66
|
+
allowBlock: false,
|
|
67
|
+
});
|
|
68
|
+
// Resolve the agent identity (machine credentials). Never blocks; attaches
|
|
69
|
+
// the agent's bearer token to outgoing Ory API calls.
|
|
70
|
+
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
71
|
+
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "pi" });
|
|
72
|
+
// Once both principals are populated, write the user→agent delegation tuple.
|
|
73
|
+
// Idempotent + fail-open: audit-trail data only.
|
|
74
|
+
await recordUserDelegatesAgent(client);
|
|
75
|
+
if (decision.mode !== "disabled") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const resolved = (0, argus_1.resolveConfig)();
|
|
79
|
+
if (resolved.auditOnly) {
|
|
80
|
+
client.logger.info("config.audit_only", {
|
|
81
|
+
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!resolved.projectUrl) {
|
|
86
|
+
client.logger.warn("config.not_configured", {
|
|
87
|
+
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
88
|
+
"Run 'npx ory-pi configure' to connect to an Ory project.",
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
93
|
+
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
94
|
+
if (sessionToken) {
|
|
95
|
+
await verifySessionToken(client, sessionToken);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (oauth2Token) {
|
|
99
|
+
await verifyOAuth2Token(client, oauth2Token);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
client.logger.warn("session.no_credentials", {
|
|
103
|
+
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
104
|
+
"Skipping authentication.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function verifySessionToken(client, token) {
|
|
108
|
+
try {
|
|
109
|
+
const session = await client.verifySession(token);
|
|
110
|
+
if (!session.active) {
|
|
111
|
+
client.logger.warn("session.inactive", {
|
|
112
|
+
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
client.logger.warn("session.verify_failed", {
|
|
118
|
+
code: isOryError(err) ? err.code : "unknown",
|
|
119
|
+
message: err instanceof Error ? err.message : String(err),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function verifyOAuth2Token(client, token) {
|
|
124
|
+
try {
|
|
125
|
+
const tokenInfo = await client.introspectToken(token);
|
|
126
|
+
if (!tokenInfo.active) {
|
|
127
|
+
client.logger.warn("oauth2.token_inactive", {
|
|
128
|
+
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
client.logger.info("oauth2.session_authenticated", {
|
|
133
|
+
clientId: tokenInfo.clientId,
|
|
134
|
+
subject: tokenInfo.subject,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
client.logger.warn("oauth2.introspect_failed", {
|
|
139
|
+
code: isOryError(err) ? err.code : "unknown",
|
|
140
|
+
message: err instanceof Error ? err.message : String(err),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ─── tool_call (pre-execution, blocking) ───────────────────────────
|
|
145
|
+
function createToolCallHandler(client) {
|
|
146
|
+
return async (event) => {
|
|
147
|
+
const toolName = event.toolName ?? "unknown";
|
|
148
|
+
// Pi does not put a session id on the tool_call event; the per-process
|
|
149
|
+
// trace context set at session start ("pi") carries correlation. We tag
|
|
150
|
+
// the span with the toolCallId so a tool_call lines up with its later
|
|
151
|
+
// tool_result.
|
|
152
|
+
client.logger.info("lifecycle.tool_call", { toolName, toolCallId: event.toolCallId });
|
|
153
|
+
client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
|
|
154
|
+
const inputSummary = (0, argus_1.summarizeToolInput)(toolName, event.input);
|
|
155
|
+
// In audit-only mode, log the invocation but skip permission checks.
|
|
156
|
+
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
157
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
158
|
+
attributes: { toolName, ...inputSummary },
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// If Ory is not configured, pass through.
|
|
163
|
+
if (!(0, argus_1.resolveConfig)().projectUrl) {
|
|
164
|
+
client.tracer.record("tool.invoke", "skipped", {
|
|
165
|
+
attributes: { toolName, reason: "not_configured", ...inputSummary },
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const subject = (0, argus_1.resolveUserSubject)(client, "agent:pi");
|
|
170
|
+
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
171
|
+
const namespace = resolveNamespace();
|
|
172
|
+
try {
|
|
173
|
+
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
174
|
+
harness: "pi",
|
|
175
|
+
toolName,
|
|
176
|
+
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
177
|
+
spanAttributes: { toolName },
|
|
178
|
+
});
|
|
179
|
+
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
180
|
+
// the user.interaction span is already recorded; pass through.
|
|
181
|
+
if (outcome.kind === "interactive") {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const decision = outcome;
|
|
185
|
+
if (decision.kind === "fail_open") {
|
|
186
|
+
handlePermissionError(decision.error, toolName, client);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const attrs = { toolName, ...inputSummary };
|
|
190
|
+
const decisionAttrs = decision.spanAttributes;
|
|
191
|
+
if (decision.kind === "allow") {
|
|
192
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
193
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (decision.kind === "observe") {
|
|
198
|
+
client.tracer.record("tool.block", "denied", {
|
|
199
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
200
|
+
});
|
|
201
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
202
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// decision.kind === "deny" — enforce: block the tool call.
|
|
207
|
+
client.tracer.record("tool.block", "denied", {
|
|
208
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
209
|
+
});
|
|
210
|
+
const reason = (0, argus_1.formatDenialMessage)({
|
|
211
|
+
tool: toolName,
|
|
212
|
+
subjectId,
|
|
213
|
+
namespace,
|
|
214
|
+
});
|
|
215
|
+
client.logger.warn("tool.denied", {
|
|
216
|
+
toolName,
|
|
217
|
+
subjectId,
|
|
218
|
+
message: reason,
|
|
219
|
+
});
|
|
220
|
+
return { block: true, reason };
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
handlePermissionError(err, toolName, client);
|
|
224
|
+
// Fail open — allow the tool call.
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ─── tool_result (post-execution, tracing) ─────────────────────────
|
|
229
|
+
function createToolResultHandler(client) {
|
|
230
|
+
return async (event) => {
|
|
231
|
+
const toolName = event.toolName ?? "unknown";
|
|
232
|
+
client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
|
|
233
|
+
// Pi reports failure via the `isError` boolean and the result payload via
|
|
234
|
+
// `content` (verified shape), not `error`/`result`.
|
|
235
|
+
const isError = event.isError === true;
|
|
236
|
+
client.logger.info("lifecycle.tool_result", {
|
|
237
|
+
toolName,
|
|
238
|
+
toolCallId: event.toolCallId,
|
|
239
|
+
isError,
|
|
240
|
+
});
|
|
241
|
+
client.tracer.record("tool.complete", isError ? "error" : "ok", {
|
|
242
|
+
attributes: {
|
|
243
|
+
toolName,
|
|
244
|
+
...(0, argus_1.summarizeToolInput)(toolName, event.input),
|
|
245
|
+
...(0, argus_1.summarizeToolOutput)(toolName, event.content),
|
|
246
|
+
...(isError ? { error: true } : {}),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
252
|
+
function resolveNamespace() {
|
|
253
|
+
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
254
|
+
}
|
|
255
|
+
function isOryError(err) {
|
|
256
|
+
return (typeof err === "object" &&
|
|
257
|
+
err !== null &&
|
|
258
|
+
"code" in err &&
|
|
259
|
+
"message" in err);
|
|
260
|
+
}
|
|
261
|
+
function handlePermissionError(err, toolName, client) {
|
|
262
|
+
if (isOryError(err)) {
|
|
263
|
+
if (err.code === "network_error" || err.code === "rate_limited") {
|
|
264
|
+
client.logger.warn("permission.fallback", {
|
|
265
|
+
toolName,
|
|
266
|
+
code: err.code,
|
|
267
|
+
message: "Failing open",
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
client.logger.error("permission.check.error", {
|
|
272
|
+
toolName,
|
|
273
|
+
code: err.code,
|
|
274
|
+
message: err.message,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
client.logger.error("permission.check.error", {
|
|
279
|
+
toolName,
|
|
280
|
+
error: err instanceof Error ? err.message : String(err),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Fail-open is log-and-allow: no tool.invoke span, matching the core
|
|
284
|
+
// gate() vocabulary. The errored permission.check span carries the audit
|
|
285
|
+
// trail for the failed check itself.
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Write the user→agent delegation tuple. Idempotent and fail-open: requires
|
|
289
|
+
* both principal subjects to be populated; any error (including an
|
|
290
|
+
* unconfigured projectUrl, which manifests as a network_error) is logged and
|
|
291
|
+
* swallowed. Purely audit-trail data — does not affect enforcement.
|
|
292
|
+
*/
|
|
293
|
+
async function recordUserDelegatesAgent(client) {
|
|
294
|
+
const user = client.userPrincipal.subject;
|
|
295
|
+
const agent = client.agentPrincipal.subject;
|
|
296
|
+
if (!user || !agent) {
|
|
297
|
+
client.logger.debug("delegation.skip", {
|
|
298
|
+
reason: "missing principal",
|
|
299
|
+
hasUser: !!user,
|
|
300
|
+
hasAgent: !!agent,
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await client.createRelationship({
|
|
306
|
+
namespace: resolveNamespace(),
|
|
307
|
+
object: `agent:${agent}`,
|
|
308
|
+
relation: "delegate",
|
|
309
|
+
subjectId: `user:${user}`,
|
|
310
|
+
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
const oryErr = err;
|
|
314
|
+
client.logger.warn("delegation.user_to_agent.failed", {
|
|
315
|
+
code: oryErr.code,
|
|
316
|
+
message: oryErr.message,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi (the minimal coding agent — pi.dev / github.com/badlogic/pi-mono) plugin
|
|
3
|
+
* types.
|
|
4
|
+
*
|
|
5
|
+
* Pi loads in-process TypeScript extensions via jiti. The module's DEFAULT
|
|
6
|
+
* EXPORT is a factory function `(pi: ExtensionAPI) => void | Promise<void>`.
|
|
7
|
+
* Pi awaits async factories before startup continues, so the factory body is
|
|
8
|
+
* the de-facto session-start hook: we run the auth gates inline there.
|
|
9
|
+
*
|
|
10
|
+
* Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
|
|
11
|
+
* `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
|
|
12
|
+
* returning nothing/undefined allows. A best-effort `tool_result` hook fires
|
|
13
|
+
* after a tool completes and is used for audit tracing.
|
|
14
|
+
*
|
|
15
|
+
* Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
|
|
16
|
+
* parsing and no sub-agent identity path.
|
|
17
|
+
*
|
|
18
|
+
* Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
|
|
19
|
+
* core/extensions/types.d.ts). The `tool_call` event "Fired before a tool
|
|
20
|
+
* executes. Can block." and a handler returns
|
|
21
|
+
* `ToolCallEventResult { block?: boolean; reason?: string }`; the
|
|
22
|
+
* `tool_result` event "Fired after a tool executes." carries `isError` and
|
|
23
|
+
* `content`. The factory default export receives an `ExtensionAPI`.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Context delivered to a `tool_call` handler (pre-execution).
|
|
27
|
+
*
|
|
28
|
+
* Verified shape: `{ type: "tool_call", toolCallId, toolName, input }`.
|
|
29
|
+
* `input` is mutable in place to patch tool arguments; we only read it. Pi
|
|
30
|
+
* does not put a session id on this event, so the plugin derives the
|
|
31
|
+
* permission subject from a stable per-process fallback.
|
|
32
|
+
*/
|
|
33
|
+
export interface PiToolCallContext {
|
|
34
|
+
/** Discriminant: always "tool_call". */
|
|
35
|
+
type?: "tool_call";
|
|
36
|
+
/** Correlates the call with its later `tool_result`. */
|
|
37
|
+
toolCallId?: string;
|
|
38
|
+
/** Tool being invoked. */
|
|
39
|
+
toolName?: string;
|
|
40
|
+
/** Tool parameters (mutable in place per Pi; the gate only reads them). */
|
|
41
|
+
input?: Record<string, unknown>;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Context delivered to a `tool_result` handler (post-execution).
|
|
46
|
+
*
|
|
47
|
+
* Verified shape: `{ type: "tool_result", toolCallId, toolName, input,
|
|
48
|
+
* content, isError, details }`. Note Pi reports failure via the `isError`
|
|
49
|
+
* boolean and the result payload via `content` (not `error`/`result`).
|
|
50
|
+
*/
|
|
51
|
+
export interface PiToolResultContext {
|
|
52
|
+
/** Discriminant: always "tool_result". */
|
|
53
|
+
type?: "tool_result";
|
|
54
|
+
/** Correlates with the originating `tool_call`. */
|
|
55
|
+
toolCallId?: string;
|
|
56
|
+
toolName?: string;
|
|
57
|
+
input?: Record<string, unknown>;
|
|
58
|
+
/** Tool output blocks (text / images). */
|
|
59
|
+
content?: unknown;
|
|
60
|
+
/** True when the tool returned an error result. */
|
|
61
|
+
isError?: boolean;
|
|
62
|
+
/** Tool-specific structured details, when present. */
|
|
63
|
+
details?: unknown;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The ExtensionContext Pi passes as the second argument to a hook (`ctx`).
|
|
68
|
+
* Modeled minimally; the plugin does not depend on it beyond optional UI.
|
|
69
|
+
*/
|
|
70
|
+
export interface PiHookRuntimeContext {
|
|
71
|
+
ui?: {
|
|
72
|
+
confirm?: (message: string) => boolean | Promise<boolean>;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
};
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The result a `tool_call` handler returns to block a tool (Pi's
|
|
79
|
+
* `ToolCallEventResult`). Returning nothing/undefined allows the tool
|
|
80
|
+
* through. Verified: `{ block?: boolean; reason?: string }`.
|
|
81
|
+
*/
|
|
82
|
+
export interface PiToolCallResult {
|
|
83
|
+
block: true;
|
|
84
|
+
reason?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Map of Pi extension event name → handler signature. Handlers may be async.
|
|
88
|
+
* Verified against Pi's `ExtensionAPI.on(...)` overloads.
|
|
89
|
+
*/
|
|
90
|
+
export interface PiEventHandlers {
|
|
91
|
+
tool_call: (event: PiToolCallContext, ctx?: PiHookRuntimeContext) => void | PiToolCallResult | Promise<void | PiToolCallResult>;
|
|
92
|
+
tool_result: (event: PiToolResultContext, ctx?: PiHookRuntimeContext) => void | Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export type PiEvent = keyof PiEventHandlers;
|
|
95
|
+
/**
|
|
96
|
+
* The ExtensionAPI object Pi passes to the default-exported factory.
|
|
97
|
+
* Verified against Pi's exported `ExtensionAPI` interface (a superset — we
|
|
98
|
+
* model only the members this plugin uses).
|
|
99
|
+
*/
|
|
100
|
+
export interface PiExtensionApi {
|
|
101
|
+
/** Subscribe to a lifecycle event. */
|
|
102
|
+
on<E extends PiEvent>(event: E, handler: PiEventHandlers[E]): void;
|
|
103
|
+
/** Register a custom tool. Unused by this plugin; modeled for completeness. */
|
|
104
|
+
registerTool?: (name: string, def: unknown) => void;
|
|
105
|
+
/** Register a custom command. Unused by this plugin; modeled for completeness. */
|
|
106
|
+
registerCommand?: (name: string, def: unknown) => void;
|
|
107
|
+
[key: string]: unknown;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The default-exported extension factory Pi invokes at load time. Pi awaits
|
|
111
|
+
* the returned promise before startup continues, so it is safe to run the
|
|
112
|
+
* session-start auth gates in the body.
|
|
113
|
+
*/
|
|
114
|
+
export type PiExtension = (pi: PiExtensionApi) => void | Promise<void>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pi (the minimal coding agent — pi.dev / github.com/badlogic/pi-mono) plugin
|
|
4
|
+
* types.
|
|
5
|
+
*
|
|
6
|
+
* Pi loads in-process TypeScript extensions via jiti. The module's DEFAULT
|
|
7
|
+
* EXPORT is a factory function `(pi: ExtensionAPI) => void | Promise<void>`.
|
|
8
|
+
* Pi awaits async factories before startup continues, so the factory body is
|
|
9
|
+
* the de-facto session-start hook: we run the auth gates inline there.
|
|
10
|
+
*
|
|
11
|
+
* Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
|
|
12
|
+
* `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
|
|
13
|
+
* returning nothing/undefined allows. A best-effort `tool_result` hook fires
|
|
14
|
+
* after a tool completes and is used for audit tracing.
|
|
15
|
+
*
|
|
16
|
+
* Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
|
|
17
|
+
* parsing and no sub-agent identity path.
|
|
18
|
+
*
|
|
19
|
+
* Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
|
|
20
|
+
* core/extensions/types.d.ts). The `tool_call` event "Fired before a tool
|
|
21
|
+
* executes. Can block." and a handler returns
|
|
22
|
+
* `ToolCallEventResult { block?: boolean; reason?: string }`; the
|
|
23
|
+
* `tool_result` event "Fired after a tool executes." carries `isError` and
|
|
24
|
+
* `content`. The factory default export receives an `ExtensionAPI`.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|