@ory/pi 0.13.9 → 1.0.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 +74 -44
- package/dist/cli/loader.d.ts +13 -4
- package/dist/cli/loader.js +20 -9
- package/dist/cli/main.js +51 -14
- package/dist/cli/setup.js +24 -3
- package/dist/plugin.d.ts +3 -34
- package/dist/plugin.js +39 -261
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Security and developer experience for [Pi](https://pi.dev), powered by [Ory](https://ory.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Pi runs real actions on your machine — editing files, running shell commands, calling APIs. This plugin gives those actions an identity, a permission check, and an audit trail.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
One command installs two independent halves:
|
|
8
|
+
|
|
9
|
+
- **Developer experience** — the Ory skill catalog, the local Ory dev stack, and activity logging. No account, no keys, nothing to configure: it works the moment it's installed.
|
|
10
|
+
- **Ory Agent Security** — browser sign-in, brokered permission checks before tool calls, and the delegation trail. Opt-in connection details from the [Ory Console](https://console.ory.sh) switch it on, and it takes nothing away from the half above. See [Connect to Ory Agent Security](#connect-to-ory-agent-security).
|
|
8
11
|
|
|
9
12
|
## What you'll need
|
|
10
13
|
|
|
@@ -21,27 +24,21 @@ npm install @ory/pi
|
|
|
21
24
|
|
|
22
25
|
## Get started
|
|
23
26
|
|
|
24
|
-
Then run one command. It installs the plugin
|
|
27
|
+
Then run one command. It installs the plugin.:
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
30
|
npx -y -p @ory/pi ory-pi install
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
The installer drops a tiny loader at `<project>/.pi/extensions/ory.js` so Pi discovers the plugin
|
|
31
|
-
|
|
32
|
-
- **Ory Network** *(default)* — sign in, or create a free account, in your browser. The project, keys, permissions, and login are all set up for you. Nothing to configure by hand.
|
|
33
|
-
- **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
|
|
34
|
-
- **Audit-only** — skip Ory entirely and just log what Pi does.
|
|
33
|
+
The installer drops a tiny loader at `<project>/.pi/extensions/ory.js` so Pi discovers the plugin and adds the Ory skills under `<project>/.pi/skills`. Add `--global` to install the loader for all projects (at `~/.pi/agent/extensions/ory.js`) instead of just this one.
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
Confirm everything landed with:
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
38
|
npx -y -p @ory/pi ory-pi status
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the loader is in place, and recent activity.
|
|
43
|
-
|
|
44
|
-
Re-run install with `--reconfigure` to change your connection later, or `--no-configure` to skip the wizard. Add `--global` to install the loader for all projects (at `~/.pi/agent/extensions/ory.js`) instead of just this one.
|
|
41
|
+
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the loader is in place, and recent activity. Until you connect Agent Security, the identity and permission rows say so and name what's missing.
|
|
45
42
|
|
|
46
43
|
<details>
|
|
47
44
|
<summary>Prefer to let Pi manage the plugin?</summary>
|
|
@@ -52,17 +49,47 @@ Point Pi's own package installer at the npm package:
|
|
|
52
49
|
pi install npm:@ory/pi
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
This registers the extension through Pi's package manifest
|
|
52
|
+
This registers the extension through Pi's package manifest. Run `npx -y -p @ory/pi ory-pi install` in a terminal afterwards to materialize the skills (and, later, `configure` if you want to connect).
|
|
56
53
|
|
|
57
54
|
</details>
|
|
58
55
|
|
|
56
|
+
## Skills and commands
|
|
57
|
+
|
|
58
|
+
Installing the plugin drops the full Ory playbook catalog into Pi. **Skills** are model-invoked — just say what you want in plain language and the matching one takes over.
|
|
59
|
+
|
|
60
|
+
| Skill | What it does for you |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
|
|
63
|
+
| `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
|
|
64
|
+
| `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
|
|
65
|
+
| `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
|
|
66
|
+
| `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
|
|
67
|
+
| `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
|
|
68
|
+
| `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
|
|
69
|
+
| `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
|
|
70
|
+
| `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
|
|
71
|
+
| `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
|
|
72
|
+
|
|
73
|
+
The local stack has its own playbooks — ask for them by name:
|
|
74
|
+
|
|
75
|
+
| Command | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `ory-local-up` | Starts a local Ory (Identities, OAuth2, Permissions) in Docker and seeds a test user — it prints the email + password to sign in with |
|
|
78
|
+
| `ory-local-down` | Stops it, keeping your data volumes |
|
|
79
|
+
| `ory-temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
|
|
80
|
+
|
|
81
|
+
The local stack runs entirely on your laptop: Ory APIs at `http://localhost:4000`, a login UI on `:4455` (not `:3000`, to dodge Next.js port clashes), and the Ory Console on `:4100`.
|
|
82
|
+
|
|
83
|
+
So: ask Pi *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
|
|
84
|
+
|
|
59
85
|
## What you get
|
|
60
86
|
|
|
61
|
-
|
|
87
|
+
Out of the box, every tool Pi runs produces a privacy-safe structured activity event in the unified local log.
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- **
|
|
89
|
+
Once you connect to Ory Agent Security, two more things happen automatically:
|
|
90
|
+
|
|
91
|
+
- **Who's driving.** You sign in once in your browser (a secure redirect flow — no passwords touch the plugin), and each Pi session gets its own identity that it registers automatically on first use. No tokens to copy around, and the "who did what" trail stays queryable later. (Pi's startup can't hard-block, so sign-in there is advisory — it still signs you in and logs it, but never stops the session.)
|
|
92
|
+
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **observe mode** — nothing is blocked, you just *see* what would be — so it never gets in your way on day one.
|
|
66
93
|
|
|
67
94
|
If Ory is ever unreachable, the plugin gets out of the way and lets Pi keep working — so it can't lock you out.
|
|
68
95
|
|
|
@@ -70,58 +97,63 @@ If Ory is ever unreachable, the plugin gets out of the way and lets Pi keep work
|
|
|
70
97
|
|
|
71
98
|
### See what's happening
|
|
72
99
|
|
|
73
|
-
Everything the plugin does is observable out of the box
|
|
100
|
+
Everything the plugin does is observable out of the box:
|
|
74
101
|
|
|
75
|
-
- **
|
|
76
|
-
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
102
|
+
- **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/pi/ory-agent-debug.log`. View events, decisions, and errors live with:
|
|
77
103
|
|
|
78
104
|
```bash
|
|
79
105
|
npx -y -p @ory/pi ory-pi watch
|
|
80
106
|
```
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
- **
|
|
108
|
+
Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
|
|
109
|
+
- **Live debug.** Launch Pi with `ORY_AGENT_DEBUG=true` to add verbose local diagnostics, including raw shell commands, to the watched log and stderr. Secrets are recursively redacted; pass `watch --json` for NDJSON.
|
|
84
110
|
|
|
85
111
|
### Ready to enforce?
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
Once connected, the deny posture lives on the Ory project: when the observe-mode activity looks right, an admin promotes it to **enforce** in the **Ory Console** (Agent Security). Every session reads that posture live, so nothing has to be reinstalled.
|
|
88
114
|
|
|
89
115
|
```bash
|
|
90
|
-
npx -y -p @ory/pi ory-pi permissions
|
|
116
|
+
npx -y -p @ory/pi ory-pi permissions # what the project grants, and the live mode
|
|
91
117
|
```
|
|
92
118
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## Also: add login to your own app
|
|
119
|
+
Then a denied tool is actually blocked and Pi shows why (the plugin returns `{ block: true, reason }` on Pi's `tool_call` event).
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
## Connect to Ory Agent Security
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
| Value | Flag | Environment variable |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Project URL | `--project-url` | `ORY_PROJECT_URL` |
|
|
128
|
+
| Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
|
|
129
|
+
| Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
|
|
104
130
|
|
|
105
131
|
```bash
|
|
106
132
|
npx -y -p @ory/pi ory-pi configure \
|
|
107
133
|
--project-url https://<slug>.projects.oryapis.com \
|
|
108
|
-
--
|
|
109
|
-
--user-login
|
|
134
|
+
--agent-security-url https://agents.console.ory.com
|
|
110
135
|
```
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
`install` accepts the same flags, so you can register the plugin **and** connect in one shot (`install --project-url <URL> --agent-security-url <URL>`). The login client defaults to `ory-agent-security-login`; custom deployments can override it with `--oauth2-client-id`. Existing configurations fall back to the project URL when the Agent Security URL is unset. To turn sign-in and checks back off later, use `configure --disconnect`.
|
|
138
|
+
|
|
139
|
+
**Ory Network or OEL.** Either works. For an Ory Network project the URL is `https://<slug>.projects.oryapis.com`; for a self-hosted **Ory Enterprise License** deployment, point `--project-url` at that deployment's base URL and use the sign-in client id from its Agent Security configuration. Everything downstream — sign-in, checks, delegation — is identical.
|
|
140
|
+
|
|
141
|
+
**There is nothing else for you to create.** The sign-in client, the permission model, the per-tool grants and blocks, and the observe/enforce posture are all provisioned in the Console by someone with project access. At runtime the plugin only *reads* permissions — it has no write path into your project, which is why installing it needs no workspace privilege. Each Pi session registers its own identity automatically on first use.
|
|
142
|
+
|
|
143
|
+
Sign-in runs at the start of every session and never blocks — a declined, skipped, or timed-out login simply leaves that session without a user identity.
|
|
113
144
|
|
|
114
|
-
|
|
145
|
+
Settings are saved to `~/.config/ory-agent-plugins/config.json` and shared across all your Ory agent plugins; environment variables win when both are set. Running headless with an OAuth2 access token already? Set `ORY_USER_OAUTH2_TOKEN` and the browser step is skipped entirely.
|
|
115
146
|
|
|
116
147
|
## Commands
|
|
117
148
|
|
|
118
149
|
```
|
|
119
|
-
ory-pi install | uninstall Install
|
|
150
|
+
ory-pi install | uninstall Install (--global for all projects; add --project-url to also connect Agent Security) / remove
|
|
120
151
|
ory-pi status Show configuration, identities, permission coverage, recent activity
|
|
121
|
-
ory-pi
|
|
122
|
-
ory-pi
|
|
123
|
-
ory-pi configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --user-login, --audit-only)
|
|
152
|
+
ory-pi permissions <cmd> status (read-only; grants + posture live in the Ory Console)
|
|
153
|
+
ory-pi configure <flags> Connect a project (--project-url) or --disconnect
|
|
124
154
|
ory-pi agent <status|unregister> Manage Pi's own auto-created identity
|
|
155
|
+
ory-pi local <up|down|status|…> Run / manage a local Ory in Docker
|
|
156
|
+
ory-pi version Print plugin, core, and Node versions (--json for machine-readable)
|
|
125
157
|
```
|
|
126
158
|
|
|
127
159
|
All prefixed with `npx -y -p @ory/pi`.
|
|
@@ -129,16 +161,14 @@ All prefixed with `npx -y -p @ory/pi`.
|
|
|
129
161
|
## Troubleshooting
|
|
130
162
|
|
|
131
163
|
- **Local Ory won't start** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
132
|
-
- **Browser sign-in loops** — reset with `ory-pi agent unregister` and try again.
|
|
133
|
-
- **
|
|
164
|
+
- **Browser sign-in loops** (after connecting) — reset with `ory-pi agent unregister` and try again.
|
|
165
|
+
- **Running an older CLI than expected** — `npx -p @ory/pi` (no version pin) reuses a previously-downloaded copy instead of re-resolving to the latest. The install banner prints the running version; confirm it with `npx -y -p @ory/pi ory-pi version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/pi ory-pi install`. Pinning an exact version (`@ory/pi@<version>`) also bypasses the cached copy.
|
|
134
166
|
- **Pi can't find the plugin** — make sure `@ory/pi` is installed in the project (`npm install @ory/pi`) and the loader exists at `.pi/extensions/ory.js`; re-run the installer if not.
|
|
135
|
-
- **Want to see what's happening** — `npx -y -p @ory/pi ory-pi status` for a snapshot, `npx -y -p @ory/pi ory-pi watch` for the live trace stream, or set `ORY_AGENT_DEBUG=true` for a verbose log. Traces and logs live under `~/.config/ory-agent-plugins/pi/` (see [See what's happening](#see-whats-happening)).
|
|
136
167
|
|
|
137
168
|
## Learn more
|
|
138
169
|
|
|
139
170
|
- [Ory documentation](https://www.ory.com/docs/) · [Ory Console](https://console.ory.sh) · [Ory Elements](https://github.com/ory/elements)
|
|
140
171
|
- [Pi repository](https://github.com/badlogic/pi-mono)
|
|
141
|
-
- [Repo README](../../README.md) and [AGENTS.md](../../AGENTS.md) — full env-var and permission-mode reference
|
|
142
172
|
|
|
143
173
|
## License
|
|
144
174
|
|
package/dist/cli/loader.d.ts
CHANGED
|
@@ -6,16 +6,23 @@
|
|
|
6
6
|
* placed there is loaded via jiti and its default export is invoked as the
|
|
7
7
|
* extension factory. A bare module id in `settings.json`'s `extensions` array is
|
|
8
8
|
* NOT loaded — those entries are resolved as filesystem paths. So we drop a
|
|
9
|
-
* tiny loader file that re-exports the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* tiny loader file that re-exports the runtime's default factory by absolute
|
|
10
|
+
* path; Pi discovers the file and jiti loads that path directly. Re-exporting
|
|
11
|
+
* the bare module id would instead depend on `@ory/pi` being resolvable from
|
|
12
|
+
* wherever Pi happens to run, which a project-local `node_modules` does not
|
|
13
|
+
* guarantee. No `settings.json` entry is required.
|
|
12
14
|
*
|
|
13
15
|
* Verified against @earendil-works/pi-coding-agent v0.80.2 (the extension
|
|
14
16
|
* loader's `discoverAndLoadExtensions` / `discoverExtensionsInDir`).
|
|
15
17
|
*/
|
|
16
18
|
export declare const EXTENSION_MODULE = "@ory/pi";
|
|
17
19
|
export declare const LOADER_FILENAME = "ory.js";
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Loader body: re-export the resolved runtime's default factory by absolute
|
|
22
|
+
* path, so Pi's auto-discovery (which scans `.pi/extensions/` for `*.js`)
|
|
23
|
+
* loads exactly the copy this install resolved.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loaderContents(entryPath: string): string;
|
|
19
26
|
/** Resolve the `.pi/extensions/` dir for the requested scope. */
|
|
20
27
|
export declare function getExtensionsDir(opts: {
|
|
21
28
|
global?: boolean;
|
|
@@ -30,6 +37,8 @@ export declare function getLoaderPath(opts: {
|
|
|
30
37
|
export declare function writeOryLoader(opts: {
|
|
31
38
|
global?: boolean;
|
|
32
39
|
projectDir: string;
|
|
40
|
+
/** Absolute path to the runtime module the loader re-exports. */
|
|
41
|
+
entryPath: string;
|
|
33
42
|
}): string;
|
|
34
43
|
/**
|
|
35
44
|
* Remove the loader file (and the `.pi/extensions/` dir if it's left empty).
|
package/dist/cli/loader.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* placed there is loaded via jiti and its default export is invoked as the
|
|
8
8
|
* extension factory. A bare module id in `settings.json`'s `extensions` array is
|
|
9
9
|
* NOT loaded — those entries are resolved as filesystem paths. So we drop a
|
|
10
|
-
* tiny loader file that re-exports the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* tiny loader file that re-exports the runtime's default factory by absolute
|
|
11
|
+
* path; Pi discovers the file and jiti loads that path directly. Re-exporting
|
|
12
|
+
* the bare module id would instead depend on `@ory/pi` being resolvable from
|
|
13
|
+
* wherever Pi happens to run, which a project-local `node_modules` does not
|
|
14
|
+
* guarantee. No `settings.json` entry is required.
|
|
13
15
|
*
|
|
14
16
|
* Verified against @earendil-works/pi-coding-agent v0.80.2 (the extension
|
|
15
17
|
* loader's `discoverAndLoadExtensions` / `discoverExtensionsInDir`).
|
|
@@ -48,7 +50,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
50
|
};
|
|
49
51
|
})();
|
|
50
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
-
exports.
|
|
53
|
+
exports.LOADER_FILENAME = exports.EXTENSION_MODULE = void 0;
|
|
54
|
+
exports.loaderContents = loaderContents;
|
|
52
55
|
exports.getExtensionsDir = getExtensionsDir;
|
|
53
56
|
exports.getLoaderPath = getLoaderPath;
|
|
54
57
|
exports.writeOryLoader = writeOryLoader;
|
|
@@ -58,11 +61,19 @@ const os = __importStar(require("node:os"));
|
|
|
58
61
|
const path = __importStar(require("node:path"));
|
|
59
62
|
exports.EXTENSION_MODULE = "@ory/pi";
|
|
60
63
|
exports.LOADER_FILENAME = "ory.js";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Loader body: re-export the resolved runtime's default factory by absolute
|
|
66
|
+
* path, so Pi's auto-discovery (which scans `.pi/extensions/` for `*.js`)
|
|
67
|
+
* loads exactly the copy this install resolved.
|
|
68
|
+
*/
|
|
69
|
+
function loaderContents(entryPath) {
|
|
70
|
+
return `// Ory extension loader for Pi. Re-exports the Ory runtime's default factory
|
|
71
|
+
// so Pi's extension auto-discovery (which scans .pi/extensions/ for *.js
|
|
72
|
+
// files) can load it. Generated by 'ory-pi install' / 'ory-pi-setup' — safe to
|
|
73
|
+
// delete. Regenerate by re-running install.
|
|
74
|
+
export { default } from ${JSON.stringify(entryPath)};
|
|
65
75
|
`;
|
|
76
|
+
}
|
|
66
77
|
/** Resolve the `.pi/extensions/` dir for the requested scope. */
|
|
67
78
|
function getExtensionsDir(opts) {
|
|
68
79
|
return opts.global
|
|
@@ -78,7 +89,7 @@ function writeOryLoader(opts) {
|
|
|
78
89
|
const dir = getExtensionsDir(opts);
|
|
79
90
|
const loaderPath = path.join(dir, exports.LOADER_FILENAME);
|
|
80
91
|
fs.mkdirSync(dir, { recursive: true });
|
|
81
|
-
fs.writeFileSync(loaderPath,
|
|
92
|
+
fs.writeFileSync(loaderPath, loaderContents(opts.entryPath));
|
|
82
93
|
return loaderPath;
|
|
83
94
|
}
|
|
84
95
|
/**
|
package/dist/cli/main.js
CHANGED
|
@@ -51,8 +51,18 @@ const loader_js_1 = require("./loader.js");
|
|
|
51
51
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
52
52
|
function main() {
|
|
53
53
|
const [command, ...args] = process.argv.slice(2);
|
|
54
|
+
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
55
|
+
// Without this the switch below ignores the flag and `install --help`
|
|
56
|
+
// performs a real install (#221).
|
|
57
|
+
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
58
|
+
return help();
|
|
54
59
|
switch (command) {
|
|
55
60
|
case "install":
|
|
61
|
+
if (args.includes("--print")) {
|
|
62
|
+
process.argv = ["node", "setup.js", ...args];
|
|
63
|
+
require("./setup.js");
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
56
66
|
(0, argus_1.beginDeferNextSteps)();
|
|
57
67
|
install(args);
|
|
58
68
|
(0, argus_1.runPostInstall)("ory-pi", "pi", args).then(() => process.exit(0), (err) => {
|
|
@@ -62,7 +72,10 @@ function main() {
|
|
|
62
72
|
break;
|
|
63
73
|
case "uninstall":
|
|
64
74
|
uninstall(args);
|
|
65
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
75
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
76
|
+
harness: "pi",
|
|
77
|
+
purge: args.includes("--purge"),
|
|
78
|
+
}).then(() => process.exit(0), (err) => {
|
|
66
79
|
console.error(err.message ?? err);
|
|
67
80
|
process.exit(1);
|
|
68
81
|
});
|
|
@@ -71,7 +84,7 @@ function main() {
|
|
|
71
84
|
(0, argus_1.runConfigureCommand)("ory-pi", args);
|
|
72
85
|
break;
|
|
73
86
|
case "agent":
|
|
74
|
-
(0, argus_1.runAgentCommand)("ory-pi", args).then((code) => process.exit(code), (err) => {
|
|
87
|
+
(0, argus_1.runAgentCommand)("ory-pi", "pi", args).then((code) => process.exit(code), (err) => {
|
|
75
88
|
console.error(err.message ?? err);
|
|
76
89
|
process.exit(1);
|
|
77
90
|
});
|
|
@@ -88,15 +101,15 @@ function main() {
|
|
|
88
101
|
process.exit(1);
|
|
89
102
|
});
|
|
90
103
|
break;
|
|
104
|
+
case "watch":
|
|
105
|
+
(0, argus_1.runWatchCommand)("ory-pi", "pi", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
106
|
+
break;
|
|
91
107
|
case "local":
|
|
92
108
|
(0, argus_1.runLocalCommand)("ory-pi", args).catch((err) => {
|
|
93
109
|
console.error(err.message ?? err);
|
|
94
110
|
process.exit(1);
|
|
95
111
|
});
|
|
96
112
|
break;
|
|
97
|
-
case "watch":
|
|
98
|
-
(0, argus_1.runWatchCommand)("pi", args);
|
|
99
|
-
break;
|
|
100
113
|
case "version":
|
|
101
114
|
case "--version":
|
|
102
115
|
case "-v":
|
|
@@ -126,23 +139,48 @@ function isGlobal(args) {
|
|
|
126
139
|
// Pi auto-discovers extensions from `<cwd>/.pi/extensions/` (project) and
|
|
127
140
|
// `~/.pi/agent/extensions/` (global): each `*.js`/`*.ts` file there is loaded
|
|
128
141
|
// via jiti and its default export is the factory. We drop a loader file that
|
|
129
|
-
// re-exports
|
|
142
|
+
// re-exports the resolved runtime's default factory by absolute path. No
|
|
143
|
+
// settings.json entry is needed.
|
|
130
144
|
// Verified against @earendil-works/pi-coding-agent v0.80.2. See ./loader.ts.
|
|
131
145
|
function loaderOpts(args) {
|
|
132
146
|
return { global: isGlobal(args), projectDir: parseProjectDir(args) };
|
|
133
147
|
}
|
|
134
148
|
function install(args) {
|
|
135
149
|
const projectDir = parseProjectDir(args);
|
|
136
|
-
|
|
150
|
+
// Pi loads the extension in-process, so there is no hook subprocess and no
|
|
151
|
+
// hook shim: the loader re-exports the runtime's main module directly. Pi
|
|
152
|
+
// also excludes MCP by design, so no MCP server is registered.
|
|
153
|
+
const runtime = (0, argus_1.wireRuntime)({
|
|
154
|
+
harness: "pi",
|
|
155
|
+
packageName: loader_js_1.EXTENSION_MODULE,
|
|
156
|
+
packageRoot: PACKAGE_ROOT,
|
|
157
|
+
installCommand: "npx -y -p @ory/pi ory-pi install",
|
|
158
|
+
args,
|
|
159
|
+
entryRelPath: "dist/index.js",
|
|
160
|
+
inProcess: true,
|
|
161
|
+
includeMcp: false,
|
|
162
|
+
});
|
|
163
|
+
console.log(runtime.target.kind === "linked"
|
|
164
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
165
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
166
|
+
for (const pruned of runtime.prunedStores) {
|
|
167
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
168
|
+
}
|
|
169
|
+
const entryPath = path.join(runtime.target.packageDir, "dist", "index.js");
|
|
170
|
+
const loaderPath = (0, loader_js_1.writeOryLoader)({ ...loaderOpts(args), entryPath });
|
|
137
171
|
(0, assets_js_1.installPiOryAssets)(projectDir);
|
|
138
172
|
console.log(`Ory extension loader installed at ${loaderPath}`);
|
|
139
|
-
console.log(` Re-exports the default factory from ${
|
|
173
|
+
console.log(` Re-exports the default factory from ${entryPath}`);
|
|
140
174
|
console.log(` Skills + commands installed under ${path.join(projectDir, ".pi", "skills")}`);
|
|
141
175
|
(0, argus_1.printEnvHelp)("ory-pi");
|
|
142
176
|
}
|
|
143
177
|
function uninstall(args) {
|
|
144
178
|
const projectDir = parseProjectDir(args);
|
|
145
179
|
const removed = (0, loader_js_1.removeOryLoader)(loaderOpts(args));
|
|
180
|
+
(0, argus_1.removeRuntimeWiring)("pi");
|
|
181
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
182
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
183
|
+
}
|
|
146
184
|
console.log(removed
|
|
147
185
|
? "Ory extension loader removed."
|
|
148
186
|
: "No Ory extension loader found. Nothing to uninstall.");
|
|
@@ -177,23 +215,22 @@ Commands:
|
|
|
177
215
|
uninstall [--project-dir <path>] [--global] Remove the Ory extension loader
|
|
178
216
|
configure Set or view Ory project URL and API key
|
|
179
217
|
agent <status|unregister> Manage the agent's OAuth2 (DCR) identity
|
|
180
|
-
permissions
|
|
181
|
-
(status, bootstrap, observe, enforce)
|
|
218
|
+
permissions Show the live permission mode and per-tool coverage
|
|
182
219
|
local <cmd> Manage local Ory dev environment
|
|
183
220
|
(up, down, status, seed, logs, env, configure, reset)
|
|
184
221
|
status [--project-dir <path>] Show plugin configuration and status
|
|
185
|
-
watch [
|
|
222
|
+
watch [--json] [--lines <count>] Follow the live activity/debug log
|
|
186
223
|
version Show version and the ory-agent-plugins build commit
|
|
187
224
|
|
|
188
225
|
After installing, the plugin hooks into these Pi lifecycle events:
|
|
189
226
|
session-start (factory body) Verify session / resolve agent identity via Ory
|
|
190
227
|
tool_call Check tool permissions via Ory Permissions (can block)
|
|
191
|
-
tool_result Record tool completion
|
|
228
|
+
tool_result Record tool completion activity
|
|
192
229
|
|
|
193
230
|
Examples:
|
|
194
231
|
npx -y -p @ory/pi ory-pi install
|
|
195
|
-
npx -y -p @ory/pi ory-pi configure --project-url https://<slug>.projects.oryapis.com --
|
|
196
|
-
npx -y -p @ory/pi ory-pi permissions
|
|
232
|
+
npx -y -p @ory/pi ory-pi configure --project-url https://<slug>.projects.oryapis.com --agent-security-url https://agents.console.ory.com
|
|
233
|
+
npx -y -p @ory/pi ory-pi permissions
|
|
197
234
|
npx -y -p @ory/pi ory-pi local up
|
|
198
235
|
npx -y -p @ory/pi ory-pi status
|
|
199
236
|
`);
|
package/dist/cli/setup.js
CHANGED
|
@@ -77,11 +77,16 @@ function main() {
|
|
|
77
77
|
}
|
|
78
78
|
const args = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
|
|
79
79
|
if (args.print) {
|
|
80
|
-
|
|
80
|
+
// Nothing is resolved or written on a --print run.
|
|
81
|
+
console.log((0, loader_js_1.loaderContents)("<resolved runtime>/dist/index.js"));
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
if (args.uninstall) {
|
|
84
85
|
const removed = (0, loader_js_1.removeOryLoader)(args);
|
|
86
|
+
(0, argus_1.removeRuntimeWiring)("pi");
|
|
87
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
88
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
89
|
+
}
|
|
85
90
|
console.log(removed
|
|
86
91
|
? "Removed Ory extension loader."
|
|
87
92
|
: "No Ory extension loader found. Nothing to uninstall.");
|
|
@@ -89,7 +94,20 @@ function main() {
|
|
|
89
94
|
console.log(`Removed Ory skills from ${path.join(args.projectDir, ".pi", "skills")}`);
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
|
-
const
|
|
97
|
+
const runtime = (0, argus_1.wireRuntime)({
|
|
98
|
+
harness: "pi",
|
|
99
|
+
packageName: "@ory/pi",
|
|
100
|
+
packageRoot: path.resolve(__dirname, "..", ".."),
|
|
101
|
+
installCommand: "npx -y -p @ory/pi ory-pi install",
|
|
102
|
+
args: process.argv.slice(2),
|
|
103
|
+
entryRelPath: "dist/index.js",
|
|
104
|
+
inProcess: true,
|
|
105
|
+
includeMcp: false,
|
|
106
|
+
});
|
|
107
|
+
const loaderPath = (0, loader_js_1.writeOryLoader)({
|
|
108
|
+
...args,
|
|
109
|
+
entryPath: path.join(runtime.target.packageDir, "dist", "index.js"),
|
|
110
|
+
});
|
|
93
111
|
(0, assets_js_1.installPiOryAssets)(args.projectDir);
|
|
94
112
|
console.log(`Ory extension loader installed at ${loaderPath}`);
|
|
95
113
|
console.log(`Ory skills installed to ${path.join(args.projectDir, ".pi", "skills")}`);
|
|
@@ -103,7 +121,10 @@ main();
|
|
|
103
121
|
// stored Ory credentials. When required by the plugin's main CLI, that
|
|
104
122
|
// command owns the purge, so the `require.main` guard prevents a double run.
|
|
105
123
|
if (require.main === module && process.argv.includes("--uninstall")) {
|
|
106
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
124
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
125
|
+
harness: "pi",
|
|
126
|
+
purge: process.argv.includes("--purge"),
|
|
127
|
+
}).then(() => process.exit(0), (err) => {
|
|
107
128
|
console.error(err.message ?? err);
|
|
108
129
|
process.exit(1);
|
|
109
130
|
});
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,42 +1,11 @@
|
|
|
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";
|
|
1
|
+
import { OryAgentClient, ensureAgentIdentity, ensureUserAuthenticated } from "@ory/argus";
|
|
25
2
|
import type { PiExtension } from "./types.js";
|
|
26
3
|
export interface CreateOryPluginDeps {
|
|
27
|
-
/** Test injection point for the user login flow. */
|
|
28
4
|
userLogin?: typeof ensureUserAuthenticated;
|
|
29
|
-
/** Test injection point for the agent identity gate. */
|
|
30
5
|
agentGate?: typeof ensureAgentIdentity;
|
|
6
|
+
/** Pin the per-process id for deterministic tests. Production mints one. */
|
|
7
|
+
sessionId?: string;
|
|
31
8
|
}
|
|
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
9
|
export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
|
|
40
10
|
projectUrl: string;
|
|
41
|
-
apiKey?: string;
|
|
42
11
|
}, deps?: CreateOryPluginDeps): PiExtension;
|
package/dist/plugin.js
CHANGED
|
@@ -1,286 +1,64 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
3
|
exports.createOryPlugin = createOryPlugin;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
5
|
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
|
-
*/
|
|
6
|
+
const HARNESS = "pi";
|
|
35
7
|
function createOryPlugin(clientOrConfig, deps = {}) {
|
|
36
8
|
return async (pi) => {
|
|
37
9
|
const client = clientOrConfig instanceof argus_1.OryAgentClient
|
|
38
10
|
? clientOrConfig
|
|
39
11
|
: clientOrConfig
|
|
40
|
-
? new argus_1.OryAgentClient({ ...clientOrConfig, harness:
|
|
41
|
-
: argus_1.OryAgentClient.fromEnv(
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
// Written at most once per install + fail-open: audit-trail data only.
|
|
74
|
-
await (0, argus_1.writeUserDelegatesAgent)(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.",
|
|
12
|
+
? new argus_1.OryAgentClient({ ...clientOrConfig, harness: HARNESS })
|
|
13
|
+
: argus_1.OryAgentClient.fromEnv(HARNESS);
|
|
14
|
+
// Pi events have no session id, so one process UUID identifies the run.
|
|
15
|
+
const sessionId = deps.sessionId ?? (0, node_crypto_1.randomUUID)();
|
|
16
|
+
await (0, argus_1.withHookContext)(client, { sessionId }, async () => {
|
|
17
|
+
client.logger.info("lifecycle.session_start", { harness: HARNESS });
|
|
18
|
+
await (0, argus_1.sessionStart)(client, {
|
|
19
|
+
harness: HARNESS,
|
|
20
|
+
binName: "ory-pi",
|
|
21
|
+
userLogin: deps.userLogin,
|
|
22
|
+
agentGate: deps.agentGate,
|
|
113
23
|
});
|
|
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
24
|
});
|
|
121
|
-
|
|
25
|
+
pi.on("tool_call", createToolCallHandler(client, sessionId));
|
|
26
|
+
pi.on("tool_result", createToolResultHandler(client, sessionId));
|
|
27
|
+
};
|
|
122
28
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
133
|
-
clientId: tokenInfo.clientId,
|
|
134
|
-
subject: tokenInfo.subject,
|
|
29
|
+
function createToolCallHandler(client, sessionId) {
|
|
30
|
+
return (event) => (0, argus_1.withHookContext)(client, { sessionId }, async () => {
|
|
31
|
+
const toolName = event.toolName ?? "unknown";
|
|
32
|
+
client.logger.info("lifecycle.tool_call", {
|
|
33
|
+
toolName,
|
|
34
|
+
toolCallId: event.toolCallId,
|
|
135
35
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
36
|
+
const result = await (0, argus_1.gate)(client, {
|
|
37
|
+
harness: HARNESS,
|
|
38
|
+
toolName,
|
|
39
|
+
toolArgs: event.input,
|
|
40
|
+
subjectFallback: "agent:pi",
|
|
41
|
+
canBlock: true,
|
|
141
42
|
});
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
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
|
-
};
|
|
43
|
+
if (result.blocked)
|
|
44
|
+
return { block: true, reason: result.denialMessage };
|
|
45
|
+
});
|
|
227
46
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return async (event) => {
|
|
47
|
+
function createToolResultHandler(client, sessionId) {
|
|
48
|
+
return (event) => (0, argus_1.withHookContext)(client, { sessionId }, async () => {
|
|
231
49
|
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
50
|
const isError = event.isError === true;
|
|
236
51
|
client.logger.info("lifecycle.tool_result", {
|
|
237
52
|
toolName,
|
|
238
53
|
toolCallId: event.toolCallId,
|
|
239
54
|
isError,
|
|
240
55
|
});
|
|
241
|
-
|
|
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", {
|
|
56
|
+
(0, argus_1.complete)(client, {
|
|
279
57
|
toolName,
|
|
280
|
-
|
|
58
|
+
input: event.input,
|
|
59
|
+
output: event.content,
|
|
60
|
+
status: isError ? "error" : "ok",
|
|
61
|
+
extraActivityAttributes: isError ? { error: true } : undefined,
|
|
281
62
|
});
|
|
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.
|
|
63
|
+
});
|
|
286
64
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
|
|
11
11
|
* `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
|
|
12
12
|
* returning nothing/undefined allows. A best-effort `tool_result` hook fires
|
|
13
|
-
* after a tool completes and is used for
|
|
13
|
+
* after a tool completes and is used for activity auditing.
|
|
14
14
|
*
|
|
15
15
|
* Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
|
|
16
16
|
* parsing and no sub-agent identity path.
|
package/dist/types.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
|
|
12
12
|
* `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
|
|
13
13
|
* returning nothing/undefined allows. A best-effort `tool_result` hook fires
|
|
14
|
-
* after a tool completes and is used for
|
|
14
|
+
* after a tool completes and is used for activity auditing.
|
|
15
15
|
*
|
|
16
16
|
* Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
|
|
17
17
|
* parsing and no sub-agent identity path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Ory plugin for Pi (the minimal coding agent): scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
"agent-security",
|
|
38
38
|
"guardrails",
|
|
39
39
|
"llm",
|
|
40
|
-
"
|
|
41
|
-
"distributed-tracing",
|
|
40
|
+
"activity-auditing",
|
|
42
41
|
"observability",
|
|
43
42
|
"kratos",
|
|
44
43
|
"keto",
|
|
@@ -49,6 +48,10 @@
|
|
|
49
48
|
"registry": "https://registry.npmjs.org/",
|
|
50
49
|
"provenance": true
|
|
51
50
|
},
|
|
51
|
+
"reova": {
|
|
52
|
+
"enabled": true,
|
|
53
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
54
|
+
},
|
|
52
55
|
"main": "dist/index.js",
|
|
53
56
|
"types": "dist/index.d.ts",
|
|
54
57
|
"exports": {
|
|
@@ -68,12 +71,11 @@
|
|
|
68
71
|
},
|
|
69
72
|
"files": [
|
|
70
73
|
"dist",
|
|
71
|
-
"!dist/dev",
|
|
72
74
|
"!dist/**/*.tsbuildinfo"
|
|
73
75
|
],
|
|
74
76
|
"dependencies": {
|
|
75
|
-
"
|
|
76
|
-
"@ory/argus": "0.
|
|
77
|
+
"reova": "^0.7.0",
|
|
78
|
+
"@ory/argus": "1.0.0"
|
|
77
79
|
},
|
|
78
80
|
"devDependencies": {
|
|
79
81
|
"typescript": "^6.0.2",
|
|
@@ -87,7 +89,6 @@
|
|
|
87
89
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
88
90
|
"test": "vitest run",
|
|
89
91
|
"test:watch": "vitest",
|
|
90
|
-
"typecheck": "tsc --noEmit"
|
|
91
|
-
"dev": "node dist/dev/launcher.js"
|
|
92
|
+
"typecheck": "tsc --noEmit"
|
|
92
93
|
}
|
|
93
94
|
}
|