@ory/gemini-cli 0.14.0 → 1.0.1
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 +71 -73
- package/dist/cli/assets.d.ts +11 -4
- package/dist/cli/assets.js +45 -30
- package/dist/cli/main.js +114 -47
- package/dist/cli/setup.js +23 -7
- package/dist/handlers.js +28 -164
- package/dist/hook.js +15 -5
- package/gemini-extension/GEMINI.md +13 -9
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Security and developer experience for [Gemini CLI](https://github.com/google-gemini/gemini-cli), powered by [Ory](https://ory.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Gemini 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, `/ory:` slash commands, the local Ory dev stack, an MCP server, 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
|
|
|
@@ -15,127 +18,123 @@ Security and developer experience for [Gemini CLI](https://github.com/google-gem
|
|
|
15
18
|
|
|
16
19
|
## Get started
|
|
17
20
|
|
|
18
|
-
Run one command
|
|
21
|
+
Run one command It installs the extension:
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
24
|
npx -y -p @ory/gemini-cli ory-gemini install
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
This registers the extension — its hooks, skills, `/ory:` slash commands, and the bundled Ory MCP server
|
|
25
|
-
|
|
26
|
-
- **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.
|
|
27
|
-
- **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
|
|
28
|
-
- **Audit-only** — skip Ory entirely and just log what Gemini does.
|
|
29
|
-
|
|
30
|
-
> No browser available (CI, SSH, headless)? The same walkthrough runs right in your terminal instead — or force it with `--no-web`.
|
|
31
|
-
|
|
32
|
-
That's it. Confirm everything landed with:
|
|
27
|
+
This registers the extension — its hooks, skills, `/ory:` slash commands, and the bundled Ory MCP server. Confirm everything landed with:
|
|
33
28
|
|
|
34
29
|
```bash
|
|
35
30
|
npx -y -p @ory/gemini-cli ory-gemini status
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the extension and skills registered, and recent activity.
|
|
33
|
+
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the extension and skills registered, and recent activity. Until you connect Agent Security, the identity and permission rows say so and name what's missing.
|
|
34
|
+
|
|
35
|
+
> Prefer to install the extension natively? Run `gemini extensions install https://github.com/ory/gemini-cli-extension`. That registers the extension the same way, with Agent Security not connected. If the `gemini` binary isn't on your `PATH`, `npx -y -p @ory/gemini-cli ory-gemini-setup` writes the extension config straight into your project's `.gemini/settings.json`.
|
|
36
|
+
|
|
37
|
+
## Skills and commands
|
|
38
|
+
|
|
39
|
+
Installing the extension drops the full Ory playbook catalog into Gemini CLI. **Skills** are model-invoked — just say what you want in plain language and the matching one takes over.
|
|
40
|
+
|
|
41
|
+
| Skill | What it does for you |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
|
|
44
|
+
| `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
|
|
45
|
+
| `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
|
|
46
|
+
| `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
|
|
47
|
+
| `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
|
|
48
|
+
| `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
|
|
49
|
+
| `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
|
|
50
|
+
| `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
|
|
51
|
+
| `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
|
|
52
|
+
| `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
|
|
53
|
+
|
|
54
|
+
**Slash commands** run the local stack directly:
|
|
55
|
+
|
|
56
|
+
| Command | What it does |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `/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 |
|
|
59
|
+
| `/ory:local-down` | Stops it, keeping your data volumes |
|
|
60
|
+
| `/ory:temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
|
|
61
|
+
|
|
62
|
+
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`.
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
A built-in **Ory MCP server** rounds it out — Gemini can manage identities, projects, and permissions straight from chat.
|
|
65
|
+
|
|
66
|
+
So: ask Gemini *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
|
|
41
67
|
|
|
42
68
|
## What you get
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
Out of the box, every tool Gemini runs produces a privacy-safe structured activity event in the unified local log.
|
|
71
|
+
|
|
72
|
+
Once you connect to Ory Agent Security, two more things happen automatically:
|
|
45
73
|
|
|
46
|
-
- **Who's driving.** You sign in once in your browser; Gemini gets its own identity
|
|
47
|
-
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **
|
|
48
|
-
- **A record of everything.** Every decision (allowed, denied, skipped) is logged as a trace you can send to Jaeger, Honeycomb, Grafana, or just a file.
|
|
74
|
+
- **Who's driving.** You sign in once in your browser; each Gemini session and typed sub-agent gets its own identity and registers automatically on first use. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later.
|
|
75
|
+
- **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.
|
|
49
76
|
|
|
50
77
|
If Ory is ever unreachable, the extension gets out of the way and lets Gemini keep working — so it can't lock you out.
|
|
51
78
|
|
|
52
79
|
### See what's happening
|
|
53
80
|
|
|
54
|
-
Everything the
|
|
81
|
+
Everything the extension does is observable out of the box:
|
|
55
82
|
|
|
56
|
-
- **
|
|
57
|
-
- **Live dashboard.** `npx -y -p @ory/gemini-cli ory-gemini dashboard` opens the same picture in your browser — configuration, identities, permission coverage, Ory service health, and the latest tool-call activity, all refreshing live. From there you can flip **enforcement** on or off, toggle **user login**, and use **Change stack** to reconnect to a different Ory — no CLI required.
|
|
58
|
-
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
83
|
+
- **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/gemini-cli/ory-agent-debug.log`. View events, decisions, and errors live with:
|
|
59
84
|
|
|
60
85
|
```bash
|
|
61
86
|
npx -y -p @ory/gemini-cli ory-gemini watch
|
|
62
87
|
```
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
- **
|
|
89
|
+
Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
|
|
90
|
+
- **Live debug.** Launch Gemini CLI 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.
|
|
66
91
|
|
|
67
92
|
### Ready to enforce?
|
|
68
93
|
|
|
69
|
-
|
|
94
|
+
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.
|
|
70
95
|
|
|
71
96
|
```bash
|
|
72
|
-
npx -y -p @ory/gemini-cli ory-gemini permissions
|
|
97
|
+
npx -y -p @ory/gemini-cli ory-gemini permissions # what the project grants, and the live mode
|
|
73
98
|
```
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
## Also: add login to your own app
|
|
100
|
+
Then a denied tool is actually blocked and Gemini shows why.
|
|
78
101
|
|
|
79
|
-
|
|
102
|
+
## Connect to Ory Agent Security
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
| Value | Flag | Environment variable |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| Project URL | `--project-url` | `ORY_PROJECT_URL` |
|
|
109
|
+
| Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
|
|
110
|
+
| Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
|
|
88
111
|
|
|
89
112
|
```bash
|
|
90
113
|
npx -y -p @ory/gemini-cli ory-gemini configure \
|
|
91
114
|
--project-url https://<slug>.projects.oryapis.com \
|
|
92
|
-
--
|
|
115
|
+
--agent-security-url https://agents.console.ory.com
|
|
93
116
|
```
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<details>
|
|
98
|
-
<summary>Create the sign-in client by hand</summary>
|
|
118
|
+
`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`.
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- `http://127.0.0.1:47823/callback`
|
|
103
|
-
- `http://127.0.0.1:47824/callback`
|
|
104
|
-
- `http://127.0.0.1:47825/callback`
|
|
105
|
-
- `http://127.0.0.1:47826/callback`
|
|
106
|
-
|
|
107
|
-
Create it with the [Ory CLI](https://www.ory.com/docs/guides/cli/installation):
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
ory create oauth2-client --project <project-id> \
|
|
111
|
-
--name "Ory Agent Security · user login (PKCE)" \
|
|
112
|
-
--grant-type authorization_code,refresh_token \
|
|
113
|
-
--response-type code \
|
|
114
|
-
--scope openid,offline_access \
|
|
115
|
-
--token-endpoint-auth-method none \
|
|
116
|
-
--redirect-uri http://127.0.0.1:47823/callback \
|
|
117
|
-
--redirect-uri http://127.0.0.1:47824/callback \
|
|
118
|
-
--redirect-uri http://127.0.0.1:47825/callback \
|
|
119
|
-
--redirect-uri http://127.0.0.1:47826/callback
|
|
120
|
-
```
|
|
120
|
+
**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.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
**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 Gemini session registers its own identity automatically on first use.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
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.
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
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.
|
|
127
127
|
|
|
128
128
|
## Commands
|
|
129
129
|
|
|
130
130
|
```
|
|
131
|
-
ory-gemini install | uninstall Install
|
|
131
|
+
ory-gemini install | uninstall Install (add --project-url to also connect Agent Security) / remove
|
|
132
132
|
ory-gemini status Show configuration, identities, permission coverage, recent activity
|
|
133
|
-
ory-gemini
|
|
134
|
-
ory-gemini
|
|
135
|
-
ory-gemini permissions <cmd> status | bootstrap | observe (watch) | enforce (block)
|
|
136
|
-
ory-gemini configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --audit-only)
|
|
133
|
+
ory-gemini permissions <cmd> status (read-only; grants + posture live in the Ory Console)
|
|
134
|
+
ory-gemini configure <flags> Connect a project (--project-url) or --disconnect
|
|
137
135
|
ory-gemini agent <status|unregister> Manage Gemini's own auto-created identity
|
|
138
136
|
ory-gemini local <up|down|status|…> Run / manage a local Ory in Docker
|
|
137
|
+
ory-gemini version Print plugin, core, and Node versions (--json for machine-readable)
|
|
139
138
|
```
|
|
140
139
|
|
|
141
140
|
All prefixed with `npx -y -p @ory/gemini-cli`.
|
|
@@ -143,9 +142,8 @@ All prefixed with `npx -y -p @ory/gemini-cli`.
|
|
|
143
142
|
## Troubleshooting
|
|
144
143
|
|
|
145
144
|
- **`/ory:local-up` fails** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
146
|
-
- **Browser sign-in loops** — reset with `ory-gemini agent unregister` and try again.
|
|
147
|
-
- **
|
|
148
|
-
- **Want to see what's happening** — `npx -y -p @ory/gemini-cli ory-gemini status` for a snapshot, `npx -y -p @ory/gemini-cli ory-gemini 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/gemini-cli/` (see [See what's happening](#see-whats-happening)).
|
|
145
|
+
- **Browser sign-in loops** (after connecting) — reset with `ory-gemini agent unregister` and try again.
|
|
146
|
+
- **Running an older CLI than expected** — `npx -p @ory/gemini-cli` (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/gemini-cli ory-gemini version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/gemini-cli ory-gemini install`. Pinning an exact version (`@ory/gemini-cli@<version>`) also bypasses the cached copy.
|
|
149
147
|
|
|
150
148
|
## Learn more
|
|
151
149
|
|
package/dist/cli/assets.d.ts
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
* registration, and extension manifest for the Gemini CLI plugin.
|
|
4
4
|
*
|
|
5
5
|
* The manifest and hooks.json are rendered at install time (not shipped
|
|
6
|
-
* static)
|
|
7
|
-
* the
|
|
8
|
-
* @
|
|
6
|
+
* static) because they carry the paths this machine's runtime resolved to:
|
|
7
|
+
* the hook command and the MCP server both point at shims written by
|
|
8
|
+
* {@link wireGeminiRuntime}, so nothing is re-resolved at run time.
|
|
9
9
|
*/
|
|
10
|
+
import { type PreparedRuntime } from "@ory/argus";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve this install's runtime and write its shims. Both entry points (the
|
|
13
|
+
* bundled extension in `main.ts` and the manual `.gemini` fallback in
|
|
14
|
+
* `setup.ts`) go through here so they wire the same runtime.
|
|
15
|
+
*/
|
|
16
|
+
export declare function wireGeminiRuntime(args: readonly string[]): PreparedRuntime;
|
|
10
17
|
/**
|
|
11
18
|
* Write all Ory skills, TOML slash commands, the rendered manifest, and
|
|
12
19
|
* the rendered hooks.json under a Gemini-conventional root containing
|
|
@@ -14,6 +21,6 @@
|
|
|
14
21
|
* `GEMINI.md`. For the bundled extension that's the `gemini-extension`
|
|
15
22
|
* directory; for the manual-setup fallback it's `<projectDir>/.gemini`.
|
|
16
23
|
*/
|
|
17
|
-
export declare function installGeminiOryAssets(root: string): void;
|
|
24
|
+
export declare function installGeminiOryAssets(root: string, runtime: PreparedRuntime): void;
|
|
18
25
|
/** Remove every skill + command this plugin installed under `root`. */
|
|
19
26
|
export declare function uninstallGeminiOryAssets(root: string): void;
|
package/dist/cli/assets.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* registration, and extension manifest for the Gemini CLI plugin.
|
|
5
5
|
*
|
|
6
6
|
* The manifest and hooks.json are rendered at install time (not shipped
|
|
7
|
-
* static)
|
|
8
|
-
* the
|
|
9
|
-
* @
|
|
7
|
+
* static) because they carry the paths this machine's runtime resolved to:
|
|
8
|
+
* the hook command and the MCP server both point at shims written by
|
|
9
|
+
* {@link wireGeminiRuntime}, so nothing is re-resolved at run time.
|
|
10
10
|
*/
|
|
11
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -42,15 +42,29 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
};
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.wireGeminiRuntime = wireGeminiRuntime;
|
|
45
46
|
exports.installGeminiOryAssets = installGeminiOryAssets;
|
|
46
47
|
exports.uninstallGeminiOryAssets = uninstallGeminiOryAssets;
|
|
47
48
|
const fs = __importStar(require("node:fs"));
|
|
48
49
|
const path = __importStar(require("node:path"));
|
|
49
50
|
const argus_1 = require("@ory/argus");
|
|
50
51
|
const PACKAGE_NAME = "@ory/gemini-cli";
|
|
51
|
-
const MCP_PACKAGE_NAME = "@ory/mcp-server";
|
|
52
|
-
const MCP_BIN_NAME = "ory-mcp-server";
|
|
53
52
|
const EXTENSION_NAME = "ory";
|
|
53
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
54
|
+
/**
|
|
55
|
+
* Resolve this install's runtime and write its shims. Both entry points (the
|
|
56
|
+
* bundled extension in `main.ts` and the manual `.gemini` fallback in
|
|
57
|
+
* `setup.ts`) go through here so they wire the same runtime.
|
|
58
|
+
*/
|
|
59
|
+
function wireGeminiRuntime(args) {
|
|
60
|
+
return (0, argus_1.wireRuntime)({
|
|
61
|
+
harness: "gemini-cli",
|
|
62
|
+
packageName: PACKAGE_NAME,
|
|
63
|
+
packageRoot: PACKAGE_ROOT,
|
|
64
|
+
installCommand: "npx -y -p @ory/gemini-cli ory-gemini install",
|
|
65
|
+
args,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
const RENDER_OPTS = {
|
|
55
69
|
binName: "ory-gemini",
|
|
56
70
|
packageName: PACKAGE_NAME,
|
|
@@ -78,12 +92,17 @@ function readPackageVersion() {
|
|
|
78
92
|
}
|
|
79
93
|
/**
|
|
80
94
|
* Render the hooks.json that Gemini CLI registers when it copies the
|
|
81
|
-
* extension into ~/.gemini/extensions/ory. The
|
|
82
|
-
*
|
|
83
|
-
*
|
|
95
|
+
* extension into ~/.gemini/extensions/ory. The command points at the runtime
|
|
96
|
+
* shim resolved at install time, so no package resolution happens per tool
|
|
97
|
+
* call.
|
|
84
98
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
// Gemini CLI's hook timeout is in MILLISECONDS (its runner defaults to 6e4 and
|
|
100
|
+
// reports "Hook timed out after ${timeout}ms"). The previous 10000 therefore
|
|
101
|
+
// meant 10 SECONDS on session start — a sixth of the harness's own default and
|
|
102
|
+
// far short of the interactive login, which it killed mid-flow (#220).
|
|
103
|
+
function renderHooks(command) {
|
|
104
|
+
const sessionStartMs = (0, argus_1.sessionStartHookTimeout)("gemini-cli", "milliseconds");
|
|
105
|
+
const toolMs = (0, argus_1.toolHookTimeout)("gemini-cli", "milliseconds");
|
|
87
106
|
const entry = (name, description, timeout) => [
|
|
88
107
|
{
|
|
89
108
|
matcher: "",
|
|
@@ -92,13 +111,13 @@ function renderHooks(version) {
|
|
|
92
111
|
];
|
|
93
112
|
return {
|
|
94
113
|
hooks: {
|
|
95
|
-
SessionStart: entry("ory-session-start", "Verify the user and agent identities via Ory",
|
|
96
|
-
SessionEnd: entry("ory-session-end", "Record session termination
|
|
97
|
-
BeforeTool: entry("ory-before-tool", "Check tool permissions via Ory Permissions",
|
|
98
|
-
AfterTool: entry("ory-after-tool", "Record
|
|
99
|
-
BeforeToolSelection: entry("ory-before-tool-selection", "
|
|
100
|
-
Notification: entry("ory-notification", "Audit permission and notification prompts",
|
|
101
|
-
PreCompress: entry("ory-pre-compress", "Audit history compaction events",
|
|
114
|
+
SessionStart: entry("ory-session-start", "Verify the user and agent identities via Ory", sessionStartMs),
|
|
115
|
+
SessionEnd: entry("ory-session-end", "Record session termination in the Ory activity log", toolMs),
|
|
116
|
+
BeforeTool: entry("ory-before-tool", "Check tool permissions via Ory Permissions", toolMs),
|
|
117
|
+
AfterTool: entry("ory-after-tool", "Record structured activity for tool executions via Ory", toolMs),
|
|
118
|
+
BeforeToolSelection: entry("ory-before-tool-selection", "Activity event for the upcoming tool-selection decision", toolMs),
|
|
119
|
+
Notification: entry("ory-notification", "Audit permission and notification prompts", toolMs),
|
|
120
|
+
PreCompress: entry("ory-pre-compress", "Audit history compaction events", toolMs),
|
|
102
121
|
},
|
|
103
122
|
};
|
|
104
123
|
}
|
|
@@ -109,25 +128,21 @@ function renderHooks(version) {
|
|
|
109
128
|
* package and tracks updates.
|
|
110
129
|
* - `contextFileName: GEMINI.md` for the model-facing context briefing.
|
|
111
130
|
* - `mcpServers.ory` so the Ory MCP server is auto-registered alongside
|
|
112
|
-
* the hooks
|
|
113
|
-
* the binary needn't be on PATH.
|
|
131
|
+
* the hooks, started from the resolved runtime via its shim.
|
|
114
132
|
* - A minimal `settings` block. Only `ORY_PROJECT_URL` is prompted now —
|
|
115
133
|
* the agent identity is resolved via Dynamic Client Registration, and
|
|
116
134
|
* the legacy ORY_API_KEY / ORY_SESSION_TOKEN prompts were teaching
|
|
117
135
|
* users the deprecated path.
|
|
118
136
|
*/
|
|
119
|
-
function renderManifest(version) {
|
|
137
|
+
function renderManifest(version, mcpServer) {
|
|
120
138
|
return {
|
|
121
139
|
name: EXTENSION_NAME,
|
|
122
140
|
version,
|
|
123
|
-
description: "Ory authentication, authorization, and
|
|
141
|
+
description: "Ory authentication, authorization, and activity auditing for Gemini CLI",
|
|
124
142
|
contextFileName: "GEMINI.md",
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
args: ["-y", "-p", `${MCP_PACKAGE_NAME}@${version}`, MCP_BIN_NAME],
|
|
129
|
-
},
|
|
130
|
-
},
|
|
143
|
+
// Registered only when the resolved runtime ships an MCP server — better
|
|
144
|
+
// no registration than one pointing at a server that cannot start.
|
|
145
|
+
...(mcpServer ? { mcpServers: { [EXTENSION_NAME]: mcpServer } } : {}),
|
|
131
146
|
settings: [
|
|
132
147
|
{
|
|
133
148
|
name: "Ory Project URL",
|
|
@@ -145,7 +160,7 @@ function renderManifest(version) {
|
|
|
145
160
|
* `GEMINI.md`. For the bundled extension that's the `gemini-extension`
|
|
146
161
|
* directory; for the manual-setup fallback it's `<projectDir>/.gemini`.
|
|
147
162
|
*/
|
|
148
|
-
function installGeminiOryAssets(root) {
|
|
163
|
+
function installGeminiOryAssets(root, runtime) {
|
|
149
164
|
const version = readPackageVersion();
|
|
150
165
|
fs.mkdirSync(root, { recursive: true });
|
|
151
166
|
const skillsDir = path.join(root, "skills");
|
|
@@ -159,8 +174,8 @@ function installGeminiOryAssets(root) {
|
|
|
159
174
|
}
|
|
160
175
|
const hooksDir = path.join(root, "hooks");
|
|
161
176
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
162
|
-
fs.writeFileSync(path.join(hooksDir, "hooks.json"), JSON.stringify(renderHooks(
|
|
163
|
-
fs.writeFileSync(path.join(root, "gemini-extension.json"), JSON.stringify(renderManifest(version), null, 2) + "\n");
|
|
177
|
+
fs.writeFileSync(path.join(hooksDir, "hooks.json"), JSON.stringify(renderHooks((0, argus_1.requireHookCommand)(runtime)), null, 2) + "\n");
|
|
178
|
+
fs.writeFileSync(path.join(root, "gemini-extension.json"), JSON.stringify(renderManifest(version, runtime.mcpServer), null, 2) + "\n");
|
|
164
179
|
}
|
|
165
180
|
function safeRmEmpty(dir) {
|
|
166
181
|
try {
|
package/dist/cli/main.js
CHANGED
|
@@ -57,10 +57,73 @@ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
|
57
57
|
* finds via the shell. POSIX shells resolve the bare name directly.
|
|
58
58
|
*/
|
|
59
59
|
const SPAWN_VIA_SHELL = process.platform === "win32";
|
|
60
|
+
/**
|
|
61
|
+
* How long to let `gemini extensions install|link` run before giving up.
|
|
62
|
+
*
|
|
63
|
+
* Gemini CLI prompts for **folder trust** on a local-path source, and that
|
|
64
|
+
* prompt does not respect a closed stdin — it blocks indefinitely rather than
|
|
65
|
+
* defaulting to "no" and exiting. With `stdio: "inherit"` and no timeout the
|
|
66
|
+
* install hung forever in any non-interactive context (#219), which is worse
|
|
67
|
+
* than failing: CI and scripted installs stall until something external kills
|
|
68
|
+
* them, and no shim is ever generated.
|
|
69
|
+
*
|
|
70
|
+
* Note this is *not* fixed by `--consent`, which covers extension security, a
|
|
71
|
+
* separate mechanism from folder trust (verified: the prompt still appears with
|
|
72
|
+
* `--consent` and with `--consent --skip-settings`).
|
|
73
|
+
*/
|
|
74
|
+
const GEMINI_CLI_TIMEOUT_MS = 45_000;
|
|
75
|
+
/**
|
|
76
|
+
* True when there is a human who could answer an interactive prompt.
|
|
77
|
+
*
|
|
78
|
+
* With no TTY on stdin nobody can respond to the folder-trust prompt, so we
|
|
79
|
+
* never start a command that will sit on it — we go straight to `manualSetup`,
|
|
80
|
+
* which produces the same working install without the harness CLI.
|
|
81
|
+
*/
|
|
82
|
+
function canAnswerPrompts() {
|
|
83
|
+
return Boolean(process.stdin.isTTY);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Run a `gemini extensions …` subcommand, returning whether it succeeded.
|
|
87
|
+
*
|
|
88
|
+
* Any non-success — a real failure, a timeout, or the binary not being
|
|
89
|
+
* spawnable — is reported as `false` so every caller degrades to `manualSetup`
|
|
90
|
+
* rather than leaving the user with a half-finished install.
|
|
91
|
+
*/
|
|
92
|
+
function runGeminiExtensions(argv) {
|
|
93
|
+
const result = (0, node_child_process_1.spawnSync)("gemini", [...argv], {
|
|
94
|
+
stdio: "inherit",
|
|
95
|
+
shell: SPAWN_VIA_SHELL,
|
|
96
|
+
timeout: GEMINI_CLI_TIMEOUT_MS,
|
|
97
|
+
});
|
|
98
|
+
if (result.status === 0)
|
|
99
|
+
return { ok: true };
|
|
100
|
+
// spawnSync reports a timeout by killing the child: `signal` is set (and
|
|
101
|
+
// `error.code` is ETIMEDOUT), with `status` null rather than a real exit code.
|
|
102
|
+
if (result.signal || result.error?.code === "ETIMEDOUT") {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: `timed out after ${GEMINI_CLI_TIMEOUT_MS / 1000}s — Gemini CLI is most likely ` +
|
|
106
|
+
`waiting on its folder-trust prompt for the extension directory`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (result.error)
|
|
110
|
+
return { ok: false, reason: result.error.message };
|
|
111
|
+
return { ok: false, reason: `exited with status ${result.status}` };
|
|
112
|
+
}
|
|
60
113
|
function main() {
|
|
61
114
|
const [command, ...args] = process.argv.slice(2);
|
|
115
|
+
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
116
|
+
// Without this the switch below ignores the flag and `install --help`
|
|
117
|
+
// performs a real install (#221).
|
|
118
|
+
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
119
|
+
return help();
|
|
62
120
|
switch (command) {
|
|
63
121
|
case "install":
|
|
122
|
+
if (args.includes("--print")) {
|
|
123
|
+
process.argv = ["node", "setup.js", ...args];
|
|
124
|
+
require("./setup.js");
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
64
127
|
(0, argus_1.beginDeferNextSteps)();
|
|
65
128
|
install(args);
|
|
66
129
|
(0, argus_1.runPostInstall)("ory-gemini", "gemini-cli", args).then(() => process.exit(0), (err) => {
|
|
@@ -70,7 +133,10 @@ function main() {
|
|
|
70
133
|
break;
|
|
71
134
|
case "uninstall":
|
|
72
135
|
uninstall(args);
|
|
73
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
136
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
137
|
+
harness: "gemini-cli",
|
|
138
|
+
purge: args.includes("--purge"),
|
|
139
|
+
}).then(() => process.exit(0), (err) => {
|
|
74
140
|
console.error(err.message ?? err);
|
|
75
141
|
process.exit(1);
|
|
76
142
|
});
|
|
@@ -79,7 +145,7 @@ function main() {
|
|
|
79
145
|
(0, argus_1.runConfigureCommand)("ory-gemini", args);
|
|
80
146
|
break;
|
|
81
147
|
case "agent":
|
|
82
|
-
(0, argus_1.runAgentCommand)("ory-gemini", args).then((code) => process.exit(code), (err) => {
|
|
148
|
+
(0, argus_1.runAgentCommand)("ory-gemini", "gemini-cli", args).then((code) => process.exit(code), (err) => {
|
|
83
149
|
console.error(err.message ?? err);
|
|
84
150
|
process.exit(1);
|
|
85
151
|
});
|
|
@@ -99,17 +165,11 @@ function main() {
|
|
|
99
165
|
process.exit(1);
|
|
100
166
|
});
|
|
101
167
|
break;
|
|
102
|
-
case "local":
|
|
103
|
-
(0, argus_1.runLocalCommand)("ory-gemini", args).catch((err) => {
|
|
104
|
-
console.error(err.message ?? err);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
});
|
|
107
|
-
break;
|
|
108
168
|
case "watch":
|
|
109
|
-
(0, argus_1.runWatchCommand)("gemini-cli", args);
|
|
169
|
+
(0, argus_1.runWatchCommand)("ory-gemini", "gemini-cli", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
110
170
|
break;
|
|
111
|
-
case "
|
|
112
|
-
(0, argus_1.
|
|
171
|
+
case "local":
|
|
172
|
+
(0, argus_1.runLocalCommand)("ory-gemini", args).catch((err) => {
|
|
113
173
|
console.error(err.message ?? err);
|
|
114
174
|
process.exit(1);
|
|
115
175
|
});
|
|
@@ -137,46 +197,53 @@ function install(args) {
|
|
|
137
197
|
// extension dir before handing it to `gemini extensions install/link`,
|
|
138
198
|
// which copies/symlinks the whole tree into ~/.gemini/extensions/ory.
|
|
139
199
|
console.log("Materializing Ory skills and commands into the extension...");
|
|
140
|
-
(0, assets_js_1.
|
|
200
|
+
const runtime = (0, assets_js_1.wireGeminiRuntime)(args);
|
|
201
|
+
console.log(runtime.target.kind === "linked"
|
|
202
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
203
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
204
|
+
for (const pruned of runtime.prunedStores) {
|
|
205
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
206
|
+
}
|
|
207
|
+
(0, assets_js_1.installGeminiOryAssets)(EXTENSION_DIR, runtime);
|
|
141
208
|
const geminiAvailable = (0, node_child_process_1.spawnSync)("gemini", ["--version"], {
|
|
142
209
|
stdio: "pipe",
|
|
143
210
|
shell: SPAWN_VIA_SHELL,
|
|
144
211
|
}).status === 0;
|
|
145
|
-
if (geminiAvailable) {
|
|
146
|
-
if (isLink) {
|
|
147
|
-
console.log("Linking Ory extension for development...");
|
|
148
|
-
const result = (0, node_child_process_1.spawnSync)("gemini", ["extensions", "link", EXTENSION_DIR], { stdio: "inherit", shell: SPAWN_VIA_SHELL });
|
|
149
|
-
if (result.status === 0) {
|
|
150
|
-
console.log("");
|
|
151
|
-
console.log("Ory extension linked for development.");
|
|
152
|
-
printQuickstart();
|
|
153
|
-
(0, argus_1.printEnvHelp)("ory-gemini");
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
console.error("gemini extensions link failed. Falling back to manual setup...");
|
|
157
|
-
manualSetup(args);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
console.log("Installing Ory extension...");
|
|
162
|
-
const result = (0, node_child_process_1.spawnSync)("gemini", ["extensions", "install", EXTENSION_DIR], { stdio: "inherit", shell: SPAWN_VIA_SHELL });
|
|
163
|
-
if (result.status === 0) {
|
|
164
|
-
console.log("");
|
|
165
|
-
console.log("Ory extension installed.");
|
|
166
|
-
printQuickstart();
|
|
167
|
-
(0, argus_1.printEnvHelp)("ory-gemini");
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
console.error("");
|
|
171
|
-
console.error("gemini extensions install failed. Falling back to manual setup...");
|
|
172
|
-
manualSetup(args);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
212
|
+
if (!geminiAvailable) {
|
|
177
213
|
console.log("Gemini CLI not found on PATH. Using manual setup...");
|
|
178
214
|
manualSetup(args);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Gemini CLI prompts for folder trust on a local-path source and will sit on
|
|
218
|
+
// that prompt forever with no one to answer it. Don't start it (#219).
|
|
219
|
+
if (!canAnswerPrompts()) {
|
|
220
|
+
console.log("No interactive terminal — skipping the Gemini CLI extension command, " +
|
|
221
|
+
"which would wait on its folder-trust prompt. Using manual setup...");
|
|
222
|
+
manualSetup(args);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const verb = isLink ? "link" : "install";
|
|
226
|
+
console.log(isLink ? "Linking Ory extension for development..." : "Installing Ory extension...");
|
|
227
|
+
// `--consent` acknowledges the extension-security prompt. It does not cover
|
|
228
|
+
// folder trust, but the user opted in by running this command, so there is no
|
|
229
|
+
// reason to ask them twice.
|
|
230
|
+
const outcome = runGeminiExtensions([
|
|
231
|
+
"extensions",
|
|
232
|
+
verb,
|
|
233
|
+
EXTENSION_DIR,
|
|
234
|
+
"--consent",
|
|
235
|
+
]);
|
|
236
|
+
if (outcome.ok) {
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(isLink ? "Ory extension linked for development." : "Ory extension installed.");
|
|
239
|
+
printQuickstart();
|
|
240
|
+
(0, argus_1.printEnvHelp)("ory-gemini");
|
|
241
|
+
return;
|
|
179
242
|
}
|
|
243
|
+
console.error("");
|
|
244
|
+
console.error(`gemini extensions ${verb} failed (${outcome.reason}).`);
|
|
245
|
+
console.error("Falling back to manual setup...");
|
|
246
|
+
manualSetup(args);
|
|
180
247
|
}
|
|
181
248
|
function uninstall(args) {
|
|
182
249
|
const geminiAvailable = (0, node_child_process_1.spawnSync)("gemini", ["--version"], {
|
|
@@ -237,11 +304,11 @@ Commands:
|
|
|
237
304
|
uninstall Remove the Ory extension from Gemini CLI
|
|
238
305
|
configure Set or view Ory project URL and API key
|
|
239
306
|
agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
|
|
240
|
-
permissions
|
|
307
|
+
permissions Show the live permission mode and per-tool coverage
|
|
241
308
|
setup [--project-dir] Write hooks directly to settings.json (fallback)
|
|
242
309
|
status Show plugin status and configuration
|
|
243
|
-
|
|
244
|
-
|
|
310
|
+
watch [--json] [--lines <count>]
|
|
311
|
+
Follow the live activity/debug log
|
|
245
312
|
version Show version and the ory-agent-plugins build commit
|
|
246
313
|
local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
|
|
247
314
|
|
package/dist/cli/setup.js
CHANGED
|
@@ -52,8 +52,7 @@ const PACKAGE_NAME = "@ory/gemini-cli";
|
|
|
52
52
|
function getSettingsPath(args) {
|
|
53
53
|
return path.join(args.projectDir, ".gemini", "settings.json");
|
|
54
54
|
}
|
|
55
|
-
function generateHooksConfig() {
|
|
56
|
-
const cmd = (0, argus_1.resolveHookCommand)(PACKAGE_NAME, BIN_NAME);
|
|
55
|
+
function generateHooksConfig(cmd) {
|
|
57
56
|
return {
|
|
58
57
|
SessionStart: (0, argus_1.matcherHookEntry)(cmd),
|
|
59
58
|
SessionEnd: (0, argus_1.matcherHookEntry)(cmd),
|
|
@@ -70,9 +69,9 @@ function main() {
|
|
|
70
69
|
process.exit(0);
|
|
71
70
|
}
|
|
72
71
|
const args = (0, argus_1.parseSetupArgs)();
|
|
73
|
-
const hooksConfig = generateHooksConfig();
|
|
74
72
|
if (args.print) {
|
|
75
|
-
|
|
73
|
+
// Nothing is resolved or written on a --print run.
|
|
74
|
+
console.log(JSON.stringify({ hooks: generateHooksConfig('node "<runtime shim>"') }, null, 2));
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
78
77
|
const settingsPath = getSettingsPath(args);
|
|
@@ -89,12 +88,26 @@ function main() {
|
|
|
89
88
|
(0, assets_js_1.uninstallGeminiOryAssets)(geminiDir);
|
|
90
89
|
console.log(`Removed Ory hooks and MCP server from ${settingsPath}`);
|
|
91
90
|
console.log(`Removed Ory skills and commands from ${geminiDir}`);
|
|
91
|
+
(0, argus_1.removeRuntimeWiring)("gemini-cli");
|
|
92
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
93
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
94
|
+
}
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
97
|
+
const runtime = (0, assets_js_1.wireGeminiRuntime)(process.argv.slice(2));
|
|
98
|
+
console.log(runtime.target.kind === "linked"
|
|
99
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
100
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
101
|
+
for (const pruned of runtime.prunedStores) {
|
|
102
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
103
|
+
}
|
|
104
|
+
const hooksConfig = generateHooksConfig((0, argus_1.requireHookCommand)(runtime));
|
|
94
105
|
let merged = (0, argus_1.mergeMatcherHooks)(existing, hooksConfig, BIN_NAME);
|
|
95
|
-
|
|
106
|
+
if (runtime.mcpServer) {
|
|
107
|
+
merged = (0, argus_1.mergeMcpServer)(merged, runtime.mcpServer);
|
|
108
|
+
}
|
|
96
109
|
(0, argus_1.writeJsonFile)(settingsPath, merged);
|
|
97
|
-
(0, assets_js_1.installGeminiOryAssets)(geminiDir);
|
|
110
|
+
(0, assets_js_1.installGeminiOryAssets)(geminiDir, runtime);
|
|
98
111
|
console.log(`Ory hooks and MCP server installed to ${settingsPath}`);
|
|
99
112
|
console.log(`Ory skills and commands installed to ${geminiDir}`);
|
|
100
113
|
(0, argus_1.printNextSteps)("Gemini CLI", "npx -y -p @ory/gemini-cli ory-gemini-setup --uninstall", {
|
|
@@ -107,7 +120,10 @@ main();
|
|
|
107
120
|
// stored Ory credentials. When required by the plugin's main CLI, that
|
|
108
121
|
// command owns the purge, so the `require.main` guard prevents a double run.
|
|
109
122
|
if (require.main === module && process.argv.includes("--uninstall")) {
|
|
110
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
123
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
124
|
+
harness: "gemini-cli",
|
|
125
|
+
purge: process.argv.includes("--purge"),
|
|
126
|
+
}).then(() => process.exit(0), (err) => {
|
|
111
127
|
console.error(err.message ?? err);
|
|
112
128
|
process.exit(1);
|
|
113
129
|
});
|
package/dist/handlers.js
CHANGED
|
@@ -12,12 +12,7 @@ async function handleHookEvent(input, client, deps = {}) {
|
|
|
12
12
|
sessionId: input.session_id,
|
|
13
13
|
toolName: input.tool_name,
|
|
14
14
|
});
|
|
15
|
-
|
|
16
|
-
client.tracer.setContext({
|
|
17
|
-
traceId: (0, argus_1.deriveTraceId)(input.session_id),
|
|
18
|
-
sessionId: input.session_id,
|
|
19
|
-
});
|
|
20
|
-
try {
|
|
15
|
+
return (0, argus_1.withHookContext)(client, { sessionId: input.session_id }, async () => {
|
|
21
16
|
switch (event) {
|
|
22
17
|
case "SessionStart":
|
|
23
18
|
return await handleSessionStart(input, client, deps);
|
|
@@ -35,15 +30,12 @@ async function handleHookEvent(input, client, deps = {}) {
|
|
|
35
30
|
return await handlePreCompress(input, client);
|
|
36
31
|
default:
|
|
37
32
|
client.logger.debug("hook.passthrough", { event });
|
|
38
|
-
client.
|
|
33
|
+
client.logger.activity("hook.passthrough", "skipped", {
|
|
39
34
|
attributes: { event },
|
|
40
35
|
});
|
|
41
36
|
return {};
|
|
42
37
|
}
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
client.tracer.clearContext();
|
|
46
|
-
}
|
|
38
|
+
});
|
|
47
39
|
}
|
|
48
40
|
// ─── SessionStart ───────────────────────────────────────────────────
|
|
49
41
|
async function handleSessionStart(input, client, deps) {
|
|
@@ -52,28 +44,13 @@ async function handleSessionStart(input, client, deps) {
|
|
|
52
44
|
source: input.source,
|
|
53
45
|
cwd: input.cwd,
|
|
54
46
|
});
|
|
55
|
-
|
|
56
|
-
attributes: { source: input.source },
|
|
57
|
-
});
|
|
58
|
-
// The user login runs every session and never blocks — it refreshes
|
|
59
|
-
// persisted tokens, prompts on a TTY when needed, and emits the user.auth
|
|
60
|
-
// audit span. Enforcement is governed solely by permissionMode at
|
|
61
|
-
// tool-call time.
|
|
62
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
63
|
-
await userGate(client, {
|
|
47
|
+
await (0, argus_1.sessionStart)(client, {
|
|
64
48
|
binName: "ory-gemini",
|
|
65
49
|
harness: "gemini-cli",
|
|
50
|
+
userLogin: deps.userLogin,
|
|
51
|
+
agentGate: deps.agentGate,
|
|
52
|
+
activityAttributes: { source: input.source },
|
|
66
53
|
});
|
|
67
|
-
// Resolve the agent identity (machine credentials). Never blocks;
|
|
68
|
-
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
69
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
70
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "gemini-cli" });
|
|
71
|
-
// Record the user→agent delegation so the audit trail captures that
|
|
72
|
-
// this user authorized this agent for this session. Written at most once
|
|
73
|
-
// per install. Best-effort: requires both principals to be populated, and
|
|
74
|
-
// any failure is logged and swallowed (fail-open — delegation tracking is
|
|
75
|
-
// for audit, not enforcement).
|
|
76
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
77
54
|
return {};
|
|
78
55
|
}
|
|
79
56
|
// ─── BeforeTool ────────────────────────────────────────────────────
|
|
@@ -84,106 +61,22 @@ async function handleBeforeTool(input, client) {
|
|
|
84
61
|
toolName,
|
|
85
62
|
toolInput: input.tool_input,
|
|
86
63
|
});
|
|
87
|
-
const inputSummary = (0, argus_1.summarizeToolInput)(toolName, input.tool_input);
|
|
88
|
-
// In audit-only mode, log the invocation but skip permission checks
|
|
89
|
-
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
90
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
91
|
-
attributes: { toolName, ...inputSummary },
|
|
92
|
-
});
|
|
93
|
-
return {};
|
|
94
|
-
}
|
|
95
|
-
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
96
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
97
64
|
const mcpTool = (0, argus_1.parseGeminiMcpTool)(toolName, input.mcp_context);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
mcpServer: mcpTool.serverName,
|
|
107
|
-
mcpTool: mcpTool.toolName,
|
|
108
|
-
...inputSummary,
|
|
109
|
-
};
|
|
110
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
111
|
-
object: mcpTool.serverName,
|
|
112
|
-
relation: "use",
|
|
113
|
-
subjectId,
|
|
114
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
115
|
-
spanAttributes: mcpAttrs,
|
|
116
|
-
});
|
|
117
|
-
const decisionAttrs = decision.spanAttributes;
|
|
118
|
-
if (decision.kind === "allow") {
|
|
119
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
120
|
-
attributes: { ...mcpAttrs, ...decisionAttrs },
|
|
121
|
-
});
|
|
122
|
-
return {};
|
|
123
|
-
}
|
|
124
|
-
if (decision.kind === "observe") {
|
|
125
|
-
client.tracer.record("tool.block", "denied", {
|
|
126
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
127
|
-
});
|
|
128
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
129
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
|
|
130
|
-
});
|
|
131
|
-
return {};
|
|
132
|
-
}
|
|
133
|
-
client.tracer.record("tool.block", "denied", {
|
|
134
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
decision: "deny",
|
|
138
|
-
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
|
|
139
|
-
systemMessage: (0, argus_1.formatDenialSummary)({ tool: toolName, subjectId, mcp: mcpTool }),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
const namespace = resolveNamespace();
|
|
143
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
144
|
-
harness: "gemini-cli",
|
|
145
|
-
toolName,
|
|
146
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
147
|
-
spanAttributes: { toolName },
|
|
148
|
-
});
|
|
149
|
-
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
150
|
-
// user.interaction span is already recorded; pass through.
|
|
151
|
-
if (outcome.kind === "interactive") {
|
|
152
|
-
return {};
|
|
153
|
-
}
|
|
154
|
-
const decision = outcome;
|
|
155
|
-
if (decision.kind === "fail_open") {
|
|
156
|
-
return handlePermissionError(decision.error, toolName, client);
|
|
157
|
-
}
|
|
158
|
-
const attrs = { toolName, ...inputSummary };
|
|
159
|
-
const decisionAttrs = decision.spanAttributes;
|
|
160
|
-
if (decision.kind === "allow") {
|
|
161
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
162
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
163
|
-
});
|
|
164
|
-
return {};
|
|
165
|
-
}
|
|
166
|
-
if (decision.kind === "observe") {
|
|
167
|
-
client.tracer.record("tool.block", "denied", {
|
|
168
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
169
|
-
});
|
|
170
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
171
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
172
|
-
});
|
|
173
|
-
return {};
|
|
174
|
-
}
|
|
175
|
-
client.tracer.record("tool.block", "denied", {
|
|
176
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
177
|
-
});
|
|
65
|
+
const result = await (0, argus_1.gate)(client, {
|
|
66
|
+
harness: "gemini-cli",
|
|
67
|
+
toolName,
|
|
68
|
+
toolArgs: input.tool_input,
|
|
69
|
+
subjectFallback: `session:${input.session_id}`,
|
|
70
|
+
mcpTool: mcpTool ?? undefined,
|
|
71
|
+
});
|
|
72
|
+
if (result.blocked && result.denialContext) {
|
|
178
73
|
return {
|
|
179
74
|
decision: "deny",
|
|
180
|
-
reason:
|
|
181
|
-
systemMessage: (0, argus_1.formatDenialSummary)(
|
|
75
|
+
reason: result.denialMessage,
|
|
76
|
+
systemMessage: (0, argus_1.formatDenialSummary)(result.denialContext),
|
|
182
77
|
};
|
|
183
78
|
}
|
|
184
|
-
|
|
185
|
-
return handlePermissionError(err, toolName, client);
|
|
186
|
-
}
|
|
79
|
+
return {};
|
|
187
80
|
}
|
|
188
81
|
// ─── AfterTool ─────────────────────────────────────────────────────
|
|
189
82
|
async function handleAfterTool(input, client) {
|
|
@@ -193,12 +86,10 @@ async function handleAfterTool(input, client) {
|
|
|
193
86
|
toolName,
|
|
194
87
|
toolResponse: input.tool_response,
|
|
195
88
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
|
|
201
|
-
},
|
|
89
|
+
(0, argus_1.complete)(client, {
|
|
90
|
+
toolName,
|
|
91
|
+
input: input.tool_input,
|
|
92
|
+
output: input.tool_response,
|
|
202
93
|
});
|
|
203
94
|
return {};
|
|
204
95
|
}
|
|
@@ -208,7 +99,7 @@ async function handleSessionEnd(input, client) {
|
|
|
208
99
|
sessionId: input.session_id,
|
|
209
100
|
reason: input.reason,
|
|
210
101
|
});
|
|
211
|
-
client.
|
|
102
|
+
client.logger.activity("session.end", "ok", {
|
|
212
103
|
attributes: { reason: input.reason },
|
|
213
104
|
});
|
|
214
105
|
return {};
|
|
@@ -219,14 +110,14 @@ async function handleSessionEnd(input, client) {
|
|
|
219
110
|
// payload (containing the candidate tool list). The hook can return
|
|
220
111
|
// `hookSpecificOutput.toolConfig.{ mode, allowedFunctionNames }` to filter
|
|
221
112
|
// the model's tool surface in one shot, collapsing many BeforeTool denials
|
|
222
|
-
// into one upstream check. We currently emit an
|
|
113
|
+
// into one upstream check. We currently emit an activity event only — a future
|
|
223
114
|
// enhancement should extract `tools` from `llm_request` and call
|
|
224
115
|
// `client.batchCheckPermissions()` to populate `allowedFunctionNames`.
|
|
225
116
|
async function handleBeforeToolSelection(input, client) {
|
|
226
117
|
client.logger.info("lifecycle.before_tool_selection", {
|
|
227
118
|
sessionId: input.session_id,
|
|
228
119
|
});
|
|
229
|
-
client.
|
|
120
|
+
client.logger.activity("permission.batch_check", "ok", {
|
|
230
121
|
attributes: { stage: "before_tool_selection" },
|
|
231
122
|
});
|
|
232
123
|
return {};
|
|
@@ -241,10 +132,10 @@ async function handleNotification(input, client) {
|
|
|
241
132
|
notificationType: input.notification_type,
|
|
242
133
|
message: input.message,
|
|
243
134
|
});
|
|
244
|
-
client.
|
|
135
|
+
client.logger.activity("notification", "ok", {
|
|
245
136
|
attributes: {
|
|
246
137
|
notificationType: input.notification_type,
|
|
247
|
-
|
|
138
|
+
messageBytes: input.message ? Buffer.byteLength(input.message, "utf8") : undefined,
|
|
248
139
|
},
|
|
249
140
|
});
|
|
250
141
|
return {};
|
|
@@ -255,35 +146,8 @@ async function handlePreCompress(input, client) {
|
|
|
255
146
|
sessionId: input.session_id,
|
|
256
147
|
trigger: input.trigger,
|
|
257
148
|
});
|
|
258
|
-
client.
|
|
149
|
+
client.logger.activity("compaction", "ok", {
|
|
259
150
|
attributes: { trigger: input.trigger, stage: "pre_compress" },
|
|
260
151
|
});
|
|
261
152
|
return {};
|
|
262
153
|
}
|
|
263
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
264
|
-
function resolveNamespace() {
|
|
265
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
266
|
-
}
|
|
267
|
-
function handlePermissionError(oryErr, toolName, client) {
|
|
268
|
-
if (oryErr.code === "network_error") {
|
|
269
|
-
client.logger.warn("permission.network_error", {
|
|
270
|
-
toolName,
|
|
271
|
-
message: "Ory unreachable, failing open",
|
|
272
|
-
});
|
|
273
|
-
return {};
|
|
274
|
-
}
|
|
275
|
-
if (oryErr.code === "rate_limited") {
|
|
276
|
-
client.logger.warn("permission.rate_limited", {
|
|
277
|
-
toolName,
|
|
278
|
-
message: "Ory rate limited, failing open",
|
|
279
|
-
});
|
|
280
|
-
return {};
|
|
281
|
-
}
|
|
282
|
-
// For other errors, also fail open but log prominently
|
|
283
|
-
client.logger.error("permission.check.error", {
|
|
284
|
-
toolName,
|
|
285
|
-
code: oryErr.code,
|
|
286
|
-
message: oryErr.message,
|
|
287
|
-
});
|
|
288
|
-
return {};
|
|
289
|
-
}
|
package/dist/hook.js
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
const argus_1 = require("@ory/argus");
|
|
12
12
|
const handlers_js_1 = require("./handlers.js");
|
|
13
|
+
const EVENT_FLUSH_TIMEOUT_MS = 750;
|
|
14
|
+
let client;
|
|
15
|
+
async function flushEvents() {
|
|
16
|
+
try {
|
|
17
|
+
await client?.flushEvents(EVENT_FLUSH_TIMEOUT_MS);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// The hook decision must not depend on activity delivery.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
/**
|
|
14
24
|
* Read all of stdin as a string.
|
|
15
25
|
*
|
|
@@ -43,7 +53,7 @@ function readStdin() {
|
|
|
43
53
|
});
|
|
44
54
|
}
|
|
45
55
|
async function main() {
|
|
46
|
-
|
|
56
|
+
client = argus_1.OryAgentClient.fromEnv("gemini-cli");
|
|
47
57
|
const raw = await readStdin();
|
|
48
58
|
let input;
|
|
49
59
|
try {
|
|
@@ -51,13 +61,12 @@ async function main() {
|
|
|
51
61
|
}
|
|
52
62
|
catch {
|
|
53
63
|
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
54
|
-
await client.tracer.shutdown();
|
|
55
64
|
process.exit(0); // Don't block on parse errors
|
|
56
65
|
}
|
|
57
66
|
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
58
67
|
if (output.decision === "deny" || output.decision === "block") {
|
|
59
68
|
process.stdout.write(JSON.stringify(output));
|
|
60
|
-
await
|
|
69
|
+
await flushEvents();
|
|
61
70
|
process.exit(2);
|
|
62
71
|
}
|
|
63
72
|
if (output.hookSpecificOutput ||
|
|
@@ -65,10 +74,11 @@ async function main() {
|
|
|
65
74
|
output.decision === "allow") {
|
|
66
75
|
process.stdout.write(JSON.stringify(output));
|
|
67
76
|
}
|
|
68
|
-
await
|
|
77
|
+
await flushEvents();
|
|
69
78
|
process.exit(0);
|
|
70
79
|
}
|
|
71
|
-
main().catch((err) => {
|
|
80
|
+
main().catch(async (err) => {
|
|
72
81
|
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
82
|
+
await flushEvents();
|
|
73
83
|
process.exit(0); // Fail open
|
|
74
84
|
});
|
|
@@ -24,25 +24,29 @@ tool call.
|
|
|
24
24
|
## Slash commands
|
|
25
25
|
|
|
26
26
|
- `/ory:local-up` — start a local Ory instance in Docker (Identities, OAuth2,
|
|
27
|
-
Permissions, plus a login UI on `:4455` (not :3000, to avoid Next.js port conflicts)
|
|
27
|
+
Permissions, plus a login UI on `:4455` (not :3000, to avoid Next.js port conflicts), reachable
|
|
28
28
|
through `http://localhost:4000`). Prints seeded test-user credentials.
|
|
29
29
|
- `/ory:local-down` — tear it all down.
|
|
30
30
|
|
|
31
31
|
## Permission mode
|
|
32
32
|
|
|
33
33
|
After install the extension runs in **observe** mode by default: permission
|
|
34
|
-
denies are recorded as
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
denies are recorded as activity events but tools still execute. The mode is a
|
|
35
|
+
property of the Ory project, read on every session — tell the user that someone
|
|
36
|
+
with access to the project promotes it to **enforce** in the Ory Console (Agent
|
|
37
|
+
Security) once the permission set is correct. There is no CLI command that sets
|
|
38
|
+
it; the plugin only reads it.
|
|
37
39
|
|
|
38
40
|
If a tool call *is* blocked, the denial reason names the missing relation.
|
|
39
|
-
Suggest `npx -y -p @ory/gemini-cli ory-gemini permissions
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
Suggest `npx -y -p @ory/gemini-cli ory-gemini permissions` to see exactly
|
|
42
|
+
which tools lack a grant, then have the grant added in the Ory Console (or via
|
|
43
|
+
the Ory MCP server, which can write relations on the user's behalf).
|
|
42
44
|
|
|
43
45
|
## Diagnostics
|
|
44
46
|
|
|
45
47
|
- `npx -y -p @ory/gemini-cli ory-gemini status` — show configuration and
|
|
46
48
|
installation state.
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
+
- Privacy-safe activity is always appended to the default
|
|
50
|
+
`~/.config/ory-agent-plugins/gemini-cli/ory-agent-debug.log`; tail it with `jq`.
|
|
51
|
+
- Launch Gemini CLI with `ORY_AGENT_DEBUG=true` for the complete live JSON stream
|
|
52
|
+
on stderr plus verbose local diagnostics. Secrets are recursively redacted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/gemini-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Ory extension for Gemini CLI: 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",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
"agent-security",
|
|
37
37
|
"guardrails",
|
|
38
38
|
"llm",
|
|
39
|
-
"
|
|
40
|
-
"distributed-tracing",
|
|
39
|
+
"activity-auditing",
|
|
41
40
|
"observability",
|
|
42
41
|
"kratos",
|
|
43
42
|
"keto",
|
|
@@ -48,6 +47,10 @@
|
|
|
48
47
|
"registry": "https://registry.npmjs.org/",
|
|
49
48
|
"provenance": true
|
|
50
49
|
},
|
|
50
|
+
"reova": {
|
|
51
|
+
"enabled": true,
|
|
52
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
53
|
+
},
|
|
51
54
|
"main": "dist/index.js",
|
|
52
55
|
"types": "dist/index.d.ts",
|
|
53
56
|
"exports": {
|
|
@@ -63,13 +66,12 @@
|
|
|
63
66
|
},
|
|
64
67
|
"files": [
|
|
65
68
|
"dist",
|
|
66
|
-
"!dist/dev",
|
|
67
69
|
"!dist/**/*.tsbuildinfo",
|
|
68
70
|
"gemini-extension"
|
|
69
71
|
],
|
|
70
72
|
"dependencies": {
|
|
71
|
-
"
|
|
72
|
-
"@ory/argus": "0.
|
|
73
|
+
"reova": "^0.7.0",
|
|
74
|
+
"@ory/argus": "1.0.1"
|
|
73
75
|
},
|
|
74
76
|
"engines": {
|
|
75
77
|
"node": ">=22"
|
|
@@ -79,7 +81,6 @@
|
|
|
79
81
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
80
82
|
"test": "vitest run",
|
|
81
83
|
"test:watch": "vitest",
|
|
82
|
-
"typecheck": "tsc --noEmit"
|
|
83
|
-
"dev": "node dist/dev/launcher.js"
|
|
84
|
+
"typecheck": "tsc --noEmit"
|
|
84
85
|
}
|
|
85
86
|
}
|