@ory/goose 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/cli/assets.d.ts +20 -0
- package/dist/cli/assets.js +90 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +180 -0
- package/dist/cli/setup.d.ts +26 -0
- package/dist/cli/setup.js +173 -0
- package/dist/handlers.d.ts +14 -0
- package/dist/handlers.js +338 -0
- package/dist/hook.d.ts +12 -0
- package/dist/hook.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +2 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Ory Agent Plugin: Goose
|
|
2
|
+
|
|
3
|
+
[Ory](https://ory.com) bundled into [Goose](https://github.com/block/goose) (Block's open-source coding agent): skills and slash commands that scaffold Ory authentication into your codebase, a local Ory stack you can spin up in one command, and (when pointed at an Ory project) authentication, authorization, and audit for every tool Goose runs.
|
|
4
|
+
|
|
5
|
+
You don't need an Ory account or any prior Ory experience to start.
|
|
6
|
+
|
|
7
|
+
This integration is verified against the `goose` binary (v1.39.0). The plugin-directory layout, the `hooks.json` schema, and the discovery paths all match the binary's shell-hook plugin contract (see [block/goose#9304](https://github.com/block/goose/pull/9304)).
|
|
8
|
+
|
|
9
|
+
## New to Ory?
|
|
10
|
+
|
|
11
|
+
[Ory](https://www.ory.com/docs/) is an open-source identity and access platform — login, registration, sessions, social sign-in, multi-factor auth, and fine-grained permissions, so you don't have to build any of that yourself. Two things make it easy to try with no prior experience:
|
|
12
|
+
|
|
13
|
+
- **Ory Elements** are prebuilt, themeable UI components for the auth pages. The scaffolding skills wire them into your app for you.
|
|
14
|
+
- **The local Ory stack** is a complete Ory running on your laptop in Docker — no account, no signup, no API key.
|
|
15
|
+
|
|
16
|
+
## What this plugin does
|
|
17
|
+
|
|
18
|
+
Two independent things, and you can use either on its own:
|
|
19
|
+
|
|
20
|
+
1. **Build auth into your app.** Have Goose scaffold Ory login, registration, social sign-in, and permissions into the project you're working on, backed by the local stack. This needs nothing but Docker.
|
|
21
|
+
2. **Govern the agent itself.** Authenticate Goose's own session and authorize every tool it runs against Ory Permissions, with a full audit trail. See [Agent security](#agent-security).
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- [Goose](https://github.com/block/goose) installed
|
|
26
|
+
- Node.js **≥ 22**
|
|
27
|
+
- [Docker](https://docs.docker.com/get-docker/) (only needed for the local Ory stack)
|
|
28
|
+
- macOS or Linux. Windows works via WSL2.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx -y -p @ory/goose ory-goose install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This is the simplest install of any harness in this repo: it drops a self-contained plugin tree at `<project>/.agents/plugins/ory/` (or `~/.agents/plugins/ory/` with `--global`) — a `plugin.json` manifest plus `hooks/hooks.json` and the Ory skills. **No config mutation, no MCP registration, no `goose configure` step.** Goose auto-discovers the plugin directory at startup.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx -y -p @ory/goose ory-goose install --global # install for all projects
|
|
40
|
+
npx -y -p @ory/goose ory-goose status # confirm what landed
|
|
41
|
+
npx -y -p @ory/goose ory-goose uninstall # remove the ory plugin dir
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`status` prints configuration, user and agent identity, per-tool permission coverage, plugin registration (project + global), and a tail of recent debug logs.
|
|
45
|
+
|
|
46
|
+
## Quickstart (≈ 3 minutes)
|
|
47
|
+
|
|
48
|
+
From any project where you'd like Ory authentication, inside Goose:
|
|
49
|
+
|
|
50
|
+
1. **Start a local Ory instance.** Ask Goose *"start the local Ory stack"* or run `/local-up`. A banner prints the seeded test user's email and password — note them.
|
|
51
|
+
2. **Scaffold Ory into your project.** Ask Goose *"add Ory auth to this app"* (the `ory-auth-setup` skill). It installs Ory Elements, wires the SDK, and generates the login / registration / recovery / settings pages, all targeting the local stack.
|
|
52
|
+
3. **Sign in.** Start your app, visit the login page, and sign in with the seeded credentials. You now have a real Ory session backed by a real Ory stack — locally, offline, zero configuration.
|
|
53
|
+
|
|
54
|
+
Continue to [Agent security](#agent-security) when you're ready to enforce.
|
|
55
|
+
|
|
56
|
+
## How the integration works
|
|
57
|
+
|
|
58
|
+
Goose discovers external **shell-hook plugins** by directory and invokes the hook binary as a subprocess for each lifecycle event (Claude-Code-style: event payload as JSON on stdin, decision on stdout). A hook blocks a tool call by returning `{"action":"block","message":"…"}` (the `RawHookAction` shape); the plugin also exits with code 2 as a defensive secondary block signal.
|
|
59
|
+
|
|
60
|
+
The `hooks.json` maps these phases to the Ory hook:
|
|
61
|
+
|
|
62
|
+
- **SessionStart** — run the user and agent auth gates.
|
|
63
|
+
- **PreToolUse** — the only blocking event; check the tool against Ory Permissions, deny blocks in enforce mode.
|
|
64
|
+
- **PostToolUse / BeforeReadFile / AfterFileEdit / BeforeShellExecution / Stop / SessionEnd** — trace-only audit spans.
|
|
65
|
+
|
|
66
|
+
The plugin is **fail-open** on its own infrastructure failures (network errors, rate limits, missing config): the agent always starts, and enforcement is only as strong as your permission grants.
|
|
67
|
+
|
|
68
|
+
## Agent security
|
|
69
|
+
|
|
70
|
+
Once pointed at an Ory project (local or hosted), Goose's session and every tool call can be governed by Ory.
|
|
71
|
+
|
|
72
|
+
- **Authentication.** The human at the keyboard (the **user**) authenticates interactively via Ory Identities when user login is on (`ORY_USER_LOGIN=true`, off by default — browser PKCE flow on first session, persisted thereafter). The Goose process (the **agent**) gets its own OAuth2 identity via [Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) on first run.
|
|
73
|
+
- **Authorization.** Before any tool runs, the plugin checks [Ory Permissions](https://www.ory.com/docs/keto) (Zanzibar-style relations) against the user's subject and blocks on `deny`.
|
|
74
|
+
- **Audit.** Every decision is recorded as a structured trace span (NDJSON file and/or OTLP export).
|
|
75
|
+
|
|
76
|
+
### Permission modes: observe → enforce
|
|
77
|
+
|
|
78
|
+
After install the plugin runs in **observe mode**: every tool call is checked, but a deny is recorded as a `permission.observe_deny` audit span and the tool runs anyway.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Grant the current user `use` on every built-in tool (idempotent):
|
|
82
|
+
npx -y -p @ory/goose ory-goose permissions bootstrap
|
|
83
|
+
|
|
84
|
+
# See allowed/denied per tool:
|
|
85
|
+
npx -y -p @ory/goose ory-goose permissions status
|
|
86
|
+
|
|
87
|
+
# Turn on hard blocking once the observe-mode logs look right:
|
|
88
|
+
npx -y -p @ory/goose ory-goose permissions enforce
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Switch back any time with `permissions observe`. To disable Ory entirely (audit logging only), run `ory-goose configure --audit-only`.
|
|
92
|
+
|
|
93
|
+
## Pointing at a real Ory project
|
|
94
|
+
|
|
95
|
+
The Quickstart uses the local stack. To point at a hosted [Ory Network](https://console.ory.sh) project:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx -y -p @ory/goose ory-goose configure \
|
|
99
|
+
--project-url https://<id>.projects.oryapis.com \
|
|
100
|
+
--oauth2-client-id <public OAuth2 client id>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--oauth2-client-id` is required whenever `--project-url` is set — the user PKCE flow needs a public OAuth2 client registered with the four loopback redirect URIs (`http://127.0.0.1:47823..47826/callback`). See the [repo README](../../README.md) and [`AGENTS.md`](../../AGENTS.md) for the full environment-variable reference and permission-mode semantics. Config is shared across every Ory agent plugin at `~/.config/ory-agent-plugins/config.json`.
|
|
104
|
+
|
|
105
|
+
Without configuration the plugin still loads cleanly and runs in **pass-through mode**: skills and commands work, but nothing is blocked.
|
|
106
|
+
|
|
107
|
+
## Links
|
|
108
|
+
|
|
109
|
+
- [Ory documentation](https://www.ory.com/docs/)
|
|
110
|
+
- [Goose repository](https://github.com/block/goose)
|
|
111
|
+
- [Repo README](../../README.md) and [AGENTS.md](../../AGENTS.md) — full env-var and permission-mode reference
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
Apache-2.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize / clean the Ory skill catalog and command-skills for the
|
|
3
|
+
* Goose plugin. The guide skills plus the local-stack commands (rendered as
|
|
4
|
+
* user-invocable skills, since Goose has no native slash-command system) land
|
|
5
|
+
* under the plugin tree at `<plugin>/skills/<name>/SKILL.md`.
|
|
6
|
+
*
|
|
7
|
+
* Goose has no native slash-command system, so we follow the Codex / OpenClaw
|
|
8
|
+
* model: wrap each command with `commandToSkill` and write it alongside the
|
|
9
|
+
* guide skills (see AGENTS.md Step 7.5).
|
|
10
|
+
*/
|
|
11
|
+
/** Skills directory under a Goose plugin root (e.g. `.agents/plugins/ory`). */
|
|
12
|
+
export declare function skillsDir(pluginRoot: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Render all Ory guide skills plus the local-stack command-skills into the
|
|
15
|
+
* plugin's `skills/` directory. Wipe-and-rewrite so renames or removals in the
|
|
16
|
+
* shared templates take effect.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installGooseOryAssets(pluginRoot: string): void;
|
|
19
|
+
/** Remove every skill + command-skill this plugin installed under `pluginRoot`. */
|
|
20
|
+
export declare function uninstallGooseOryAssets(pluginRoot: string): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Materialize / clean the Ory skill catalog and command-skills for the
|
|
4
|
+
* Goose plugin. The guide skills plus the local-stack commands (rendered as
|
|
5
|
+
* user-invocable skills, since Goose has no native slash-command system) land
|
|
6
|
+
* under the plugin tree at `<plugin>/skills/<name>/SKILL.md`.
|
|
7
|
+
*
|
|
8
|
+
* Goose has no native slash-command system, so we follow the Codex / OpenClaw
|
|
9
|
+
* model: wrap each command with `commandToSkill` and write it alongside the
|
|
10
|
+
* guide skills (see AGENTS.md Step 7.5).
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.skillsDir = skillsDir;
|
|
47
|
+
exports.installGooseOryAssets = installGooseOryAssets;
|
|
48
|
+
exports.uninstallGooseOryAssets = uninstallGooseOryAssets;
|
|
49
|
+
const fs = __importStar(require("node:fs"));
|
|
50
|
+
const path = __importStar(require("node:path"));
|
|
51
|
+
const argus_1 = require("@ory/argus");
|
|
52
|
+
const RENDER_OPTS = {
|
|
53
|
+
binName: "ory-goose",
|
|
54
|
+
packageName: "@ory/goose",
|
|
55
|
+
};
|
|
56
|
+
/** Skills directory under a Goose plugin root (e.g. `.agents/plugins/ory`). */
|
|
57
|
+
function skillsDir(pluginRoot) {
|
|
58
|
+
return path.join(pluginRoot, "skills");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render all Ory guide skills plus the local-stack command-skills into the
|
|
62
|
+
* plugin's `skills/` directory. Wipe-and-rewrite so renames or removals in the
|
|
63
|
+
* shared templates take effect.
|
|
64
|
+
*/
|
|
65
|
+
function installGooseOryAssets(pluginRoot) {
|
|
66
|
+
const dir = skillsDir(pluginRoot);
|
|
67
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
const guideSkills = (0, argus_1.renderOrySkills)("goose", RENDER_OPTS);
|
|
70
|
+
const commandSkills = (0, argus_1.renderOryCommands)("goose", RENDER_OPTS).map(argus_1.commandToSkill);
|
|
71
|
+
(0, argus_1.writeSkillTree)(dir, [...guideSkills, ...commandSkills]);
|
|
72
|
+
}
|
|
73
|
+
function safeRmEmpty(dir) {
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
|
|
76
|
+
fs.rmdirSync(dir);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* ignore — leave the dir if removal fails. */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Remove every skill + command-skill this plugin installed under `pluginRoot`. */
|
|
84
|
+
function uninstallGooseOryAssets(pluginRoot) {
|
|
85
|
+
const dir = skillsDir(pluginRoot);
|
|
86
|
+
if (fs.existsSync(dir)) {
|
|
87
|
+
(0, argus_1.removeSkillDirs)(dir, [...argus_1.ORY_SKILL_NAMES, ...argus_1.ORY_COMMAND_SKILL_NAMES]);
|
|
88
|
+
safeRmEmpty(dir);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Main CLI for @ory/goose.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx ory-goose install [--global] Install the Ory plugin for Goose
|
|
7
|
+
* npx ory-goose uninstall [--global] Remove the Ory plugin
|
|
8
|
+
* npx ory-goose configure Set or view Ory credentials
|
|
9
|
+
* npx ory-goose setup [--project-dir] Write the plugin tree directly
|
|
10
|
+
* npx ory-goose status Show plugin status
|
|
11
|
+
*
|
|
12
|
+
* Goose discovers external shell-hook plugins from `.agents/plugins/<name>/`
|
|
13
|
+
* (project-local) or `~/.agents/plugins/<name>/` (global). There is no native
|
|
14
|
+
* `goose` install command, so `install` simply materializes that tree.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Main CLI for @ory/goose.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx ory-goose install [--global] Install the Ory plugin for Goose
|
|
8
|
+
* npx ory-goose uninstall [--global] Remove the Ory plugin
|
|
9
|
+
* npx ory-goose configure Set or view Ory credentials
|
|
10
|
+
* npx ory-goose setup [--project-dir] Write the plugin tree directly
|
|
11
|
+
* npx ory-goose status Show plugin status
|
|
12
|
+
*
|
|
13
|
+
* Goose discovers external shell-hook plugins from `.agents/plugins/<name>/`
|
|
14
|
+
* (project-local) or `~/.agents/plugins/<name>/` (global). There is no native
|
|
15
|
+
* `goose` install command, so `install` simply materializes that tree.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const os = __importStar(require("node:os"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const argus_1 = require("@ory/argus");
|
|
55
|
+
const setup_js_1 = require("./setup.js");
|
|
56
|
+
const PLUGIN_NAME = "ory";
|
|
57
|
+
function main() {
|
|
58
|
+
const [command, ...args] = process.argv.slice(2);
|
|
59
|
+
switch (command) {
|
|
60
|
+
case "install":
|
|
61
|
+
install(args);
|
|
62
|
+
postInstallPermissions("ory-goose", "goose").then(() => process.exit(0), (err) => {
|
|
63
|
+
console.error(err.message ?? err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
case "uninstall":
|
|
68
|
+
uninstall(args);
|
|
69
|
+
break;
|
|
70
|
+
case "configure":
|
|
71
|
+
(0, argus_1.runConfigureCommand)("ory-goose", args);
|
|
72
|
+
break;
|
|
73
|
+
case "agent":
|
|
74
|
+
(0, argus_1.runAgentCommand)("ory-goose", args).then((code) => process.exit(code), (err) => {
|
|
75
|
+
console.error(err.message ?? err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
case "permissions":
|
|
80
|
+
(0, argus_1.runPermissionsCommand)("ory-goose", "goose", args).then((code) => process.exit(code), (err) => {
|
|
81
|
+
console.error(err.message ?? err);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
break;
|
|
85
|
+
case "setup":
|
|
86
|
+
(0, setup_js_1.runSetup)((0, argus_1.parseSetupArgs)({ supportsGlobal: true }));
|
|
87
|
+
break;
|
|
88
|
+
case "status":
|
|
89
|
+
status().then(() => process.exit(0), (err) => {
|
|
90
|
+
console.error(err.message ?? err);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
case "local":
|
|
95
|
+
(0, argus_1.runLocalCommand)("ory-goose", args).catch((err) => {
|
|
96
|
+
console.error(err.message ?? err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
case "help":
|
|
101
|
+
case "--help":
|
|
102
|
+
case "-h":
|
|
103
|
+
case undefined:
|
|
104
|
+
help();
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
console.error(`Unknown command: ${command}`);
|
|
108
|
+
help();
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function install(_args) {
|
|
113
|
+
console.log("Materializing the Ory plugin tree for Goose...");
|
|
114
|
+
(0, setup_js_1.runSetup)((0, argus_1.parseSetupArgs)({ supportsGlobal: true }));
|
|
115
|
+
(0, argus_1.printEnvHelp)("ory-goose");
|
|
116
|
+
}
|
|
117
|
+
function uninstall(_args) {
|
|
118
|
+
const setupArgs = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
|
|
119
|
+
(0, setup_js_1.runSetup)({ ...setupArgs, uninstall: true });
|
|
120
|
+
}
|
|
121
|
+
async function postInstallPermissions(binName, harness) {
|
|
122
|
+
const bootstrapped = await (0, argus_1.maybeAutoBootstrap)(binName, harness);
|
|
123
|
+
(0, argus_1.printPermissionsOnboardingHelp)(binName, harness, {
|
|
124
|
+
bootstrappedAutomatically: bootstrapped,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function pluginRoots() {
|
|
128
|
+
return {
|
|
129
|
+
project: path.join(process.cwd(), ".agents", "plugins", PLUGIN_NAME),
|
|
130
|
+
global: path.join(os.homedir(), ".agents", "plugins", PLUGIN_NAME),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function status() {
|
|
134
|
+
await (0, argus_1.runStatusCommand)("ory-goose", "goose", {
|
|
135
|
+
title: "Goose",
|
|
136
|
+
printPluginSection: () => {
|
|
137
|
+
const roots = pluginRoots();
|
|
138
|
+
console.log("Plugin:");
|
|
139
|
+
for (const [label, root] of [
|
|
140
|
+
["Project", roots.project],
|
|
141
|
+
["Global", roots.global],
|
|
142
|
+
]) {
|
|
143
|
+
const manifest = fs.existsSync(path.join(root, "plugin.json"));
|
|
144
|
+
const hooks = fs.existsSync(path.join(root, "hooks", "hooks.json"));
|
|
145
|
+
console.log(` ${label}: ${manifest && hooks ? `installed (${root})` : "not installed"}`);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function help() {
|
|
151
|
+
console.log(`
|
|
152
|
+
ory-goose — Ory plugin for Goose
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
npx ory-goose <command> [options]
|
|
156
|
+
|
|
157
|
+
Commands:
|
|
158
|
+
install [--global] Install the Ory plugin (project-local, or --global)
|
|
159
|
+
uninstall [--global] Remove the Ory plugin
|
|
160
|
+
configure Set or view Ory project URL and API key
|
|
161
|
+
agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
|
|
162
|
+
permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
|
|
163
|
+
setup [--project-dir] Write the plugin tree directly (advanced)
|
|
164
|
+
status Show plugin status and configuration
|
|
165
|
+
local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
|
|
166
|
+
|
|
167
|
+
After installing, Goose auto-discovers the plugin and hooks into these
|
|
168
|
+
lifecycle events:
|
|
169
|
+
SessionStart Verify the user and agent identities via Ory
|
|
170
|
+
PreToolUse Check tool permissions via Ory Permissions
|
|
171
|
+
PostToolUse Audit log tool executions
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
npx ory-goose install # Install into ./.agents/plugins/ory
|
|
175
|
+
npx ory-goose install --global # Install into ~/.agents/plugins/ory
|
|
176
|
+
npx ory-goose status # Check configuration
|
|
177
|
+
npx ory-goose uninstall # Remove the plugin
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
180
|
+
main();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Setup CLI for the Ory Goose plugin.
|
|
4
|
+
*
|
|
5
|
+
* Goose auto-discovers external shell-hook plugins at startup from:
|
|
6
|
+
* - `<project>/.agents/plugins/<name>/` (project-local)
|
|
7
|
+
* - `~/.agents/plugins/<name>/` (user-global, with --global)
|
|
8
|
+
*
|
|
9
|
+
* Each plugin dir holds a `plugin.json` manifest and `hooks/hooks.json`
|
|
10
|
+
* describing the hook commands. We render both fresh at install time so the
|
|
11
|
+
* version pin baked into the hook command always matches the installed
|
|
12
|
+
* package. No `goose configure`, config edit, or MCP registration is needed
|
|
13
|
+
* for the gate — discovery is purely directory-based.
|
|
14
|
+
*
|
|
15
|
+
* The plugin dir layout (plugin.json + hooks/hooks.json) and discovery paths
|
|
16
|
+
* are verified against goose v1.39.0.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* npx ory-goose-setup # Install into <cwd>/.agents/plugins/ory
|
|
20
|
+
* npx ory-goose-setup --global # Install into ~/.agents/plugins/ory
|
|
21
|
+
* npx ory-goose-setup --project-dir /repo
|
|
22
|
+
* npx ory-goose-setup --print # Print plugin.json + hooks.json to stdout
|
|
23
|
+
* npx ory-goose-setup --uninstall # Remove the ory plugin dir
|
|
24
|
+
*/
|
|
25
|
+
import { type SetupArgs } from "@ory/argus";
|
|
26
|
+
export declare function runSetup(args: SetupArgs): void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Setup CLI for the Ory Goose plugin.
|
|
5
|
+
*
|
|
6
|
+
* Goose auto-discovers external shell-hook plugins at startup from:
|
|
7
|
+
* - `<project>/.agents/plugins/<name>/` (project-local)
|
|
8
|
+
* - `~/.agents/plugins/<name>/` (user-global, with --global)
|
|
9
|
+
*
|
|
10
|
+
* Each plugin dir holds a `plugin.json` manifest and `hooks/hooks.json`
|
|
11
|
+
* describing the hook commands. We render both fresh at install time so the
|
|
12
|
+
* version pin baked into the hook command always matches the installed
|
|
13
|
+
* package. No `goose configure`, config edit, or MCP registration is needed
|
|
14
|
+
* for the gate — discovery is purely directory-based.
|
|
15
|
+
*
|
|
16
|
+
* The plugin dir layout (plugin.json + hooks/hooks.json) and discovery paths
|
|
17
|
+
* are verified against goose v1.39.0.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* npx ory-goose-setup # Install into <cwd>/.agents/plugins/ory
|
|
21
|
+
* npx ory-goose-setup --global # Install into ~/.agents/plugins/ory
|
|
22
|
+
* npx ory-goose-setup --project-dir /repo
|
|
23
|
+
* npx ory-goose-setup --print # Print plugin.json + hooks.json to stdout
|
|
24
|
+
* npx ory-goose-setup --uninstall # Remove the ory plugin dir
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.runSetup = runSetup;
|
|
61
|
+
const fs = __importStar(require("node:fs"));
|
|
62
|
+
const os = __importStar(require("node:os"));
|
|
63
|
+
const path = __importStar(require("node:path"));
|
|
64
|
+
const argus_1 = require("@ory/argus");
|
|
65
|
+
const assets_js_1 = require("./assets.js");
|
|
66
|
+
const PACKAGE_NAME = "@ory/goose";
|
|
67
|
+
const HOOK_BIN = "ory-goose-hook";
|
|
68
|
+
const PLUGIN_NAME = "ory";
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the Goose plugin root: `~/.agents/plugins/ory` with --global, else
|
|
71
|
+
* `<projectDir>/.agents/plugins/ory`.
|
|
72
|
+
*/
|
|
73
|
+
function getPluginRoot(args) {
|
|
74
|
+
const base = args.global ? os.homedir() : args.projectDir;
|
|
75
|
+
return path.join(base, ".agents", "plugins", PLUGIN_NAME);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the plugin's own version from its package.json, walking up from
|
|
79
|
+
* dist/cli to the package root. Used to version-pin the npx invocation in
|
|
80
|
+
* hooks.json so installs keep running against the version they registered.
|
|
81
|
+
*/
|
|
82
|
+
function readPackageVersion() {
|
|
83
|
+
try {
|
|
84
|
+
const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
|
|
85
|
+
const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
86
|
+
return parsed.version ?? "0.0.0";
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return "0.0.0";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Render plugin.json — Goose's plugin manifest. Names and describes the
|
|
94
|
+
* plugin so `goose` can list it.
|
|
95
|
+
*/
|
|
96
|
+
function renderManifest(version) {
|
|
97
|
+
return {
|
|
98
|
+
name: PLUGIN_NAME,
|
|
99
|
+
version,
|
|
100
|
+
description: "Ory authentication, authorization, and distributed tracing for Goose",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Render hooks/hooks.json. Each event maps to the version-pinned hook binary,
|
|
105
|
+
* resolved via npx so it needn't be on PATH after install. PreToolUse is the
|
|
106
|
+
* only event that can block; the rest are observability hooks.
|
|
107
|
+
*
|
|
108
|
+
* The hooks.json schema (per-event arrays of `{ type, command }`, optional
|
|
109
|
+
* regex `matcher`/`pattern`) is verified against goose v1.39.0.
|
|
110
|
+
*/
|
|
111
|
+
function renderHooks(version) {
|
|
112
|
+
const command = `npx -y -p ${PACKAGE_NAME}@${version} ${HOOK_BIN}`;
|
|
113
|
+
const entry = [{ type: "command", command }];
|
|
114
|
+
return {
|
|
115
|
+
hooks: {
|
|
116
|
+
SessionStart: entry,
|
|
117
|
+
SessionEnd: entry,
|
|
118
|
+
Stop: entry,
|
|
119
|
+
UserPromptSubmit: entry,
|
|
120
|
+
PreToolUse: entry,
|
|
121
|
+
PostToolUse: entry,
|
|
122
|
+
PostToolUseFailure: entry,
|
|
123
|
+
BeforeReadFile: entry,
|
|
124
|
+
AfterFileEdit: entry,
|
|
125
|
+
BeforeShellExecution: entry,
|
|
126
|
+
AfterShellExecution: entry,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function writePluginTree(pluginRoot, version) {
|
|
131
|
+
fs.mkdirSync(pluginRoot, { recursive: true });
|
|
132
|
+
fs.writeFileSync(path.join(pluginRoot, "plugin.json"), JSON.stringify(renderManifest(version), null, 2) + "\n");
|
|
133
|
+
const hooksDir = path.join(pluginRoot, "hooks");
|
|
134
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(hooksDir, "hooks.json"), JSON.stringify(renderHooks(version), null, 2) + "\n");
|
|
136
|
+
(0, assets_js_1.installGooseOryAssets)(pluginRoot);
|
|
137
|
+
}
|
|
138
|
+
function runSetup(args) {
|
|
139
|
+
const version = readPackageVersion();
|
|
140
|
+
if (args.print) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
"plugin.json": renderManifest(version),
|
|
143
|
+
"hooks/hooks.json": renderHooks(version),
|
|
144
|
+
}, null, 2));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const pluginRoot = getPluginRoot(args);
|
|
148
|
+
if (args.uninstall) {
|
|
149
|
+
if (!fs.existsSync(pluginRoot)) {
|
|
150
|
+
console.log("No Ory plugin found. Nothing to uninstall.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
(0, assets_js_1.uninstallGooseOryAssets)(pluginRoot);
|
|
154
|
+
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
|
155
|
+
console.log(`Removed Ory plugin from ${pluginRoot}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
writePluginTree(pluginRoot, version);
|
|
159
|
+
console.log(`Ory plugin installed to ${pluginRoot}`);
|
|
160
|
+
console.log(` plugin.json + hooks/hooks.json + skills/ written.`);
|
|
161
|
+
(0, argus_1.printNextSteps)("Goose", "npx ory-goose-setup --uninstall");
|
|
162
|
+
}
|
|
163
|
+
function main() {
|
|
164
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
165
|
+
(0, argus_1.printSetupHelp)("ory-goose-setup", "Goose");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
const args = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
|
|
169
|
+
runSetup(args);
|
|
170
|
+
}
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
main();
|
|
173
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
|
|
2
|
+
import type { GooseHookInput, GooseHookOutput } from "./types.js";
|
|
3
|
+
export interface HandleHookEventDeps {
|
|
4
|
+
/** Test injection point for the user login flow. */
|
|
5
|
+
userLogin?: typeof ensureUserAuthenticated;
|
|
6
|
+
/** Test injection point for the agent identity gate. */
|
|
7
|
+
agentGate?: typeof ensureAgentIdentity;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Route a Goose hook event to the appropriate Ory integration.
|
|
11
|
+
*
|
|
12
|
+
* Event-name set verified against goose v1.39.0.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleHookEvent(input: GooseHookInput, client: OryAgentClient, deps?: HandleHookEventDeps): Promise<GooseHookOutput>;
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleHookEvent = handleHookEvent;
|
|
4
|
+
const argus_1 = require("@ory/argus");
|
|
5
|
+
/**
|
|
6
|
+
* Route a Goose hook event to the appropriate Ory integration.
|
|
7
|
+
*
|
|
8
|
+
* Event-name set verified against goose v1.39.0.
|
|
9
|
+
*/
|
|
10
|
+
async function handleHookEvent(input, client, deps = {}) {
|
|
11
|
+
const event = input.event;
|
|
12
|
+
client.logger.debug("hook.received", {
|
|
13
|
+
event,
|
|
14
|
+
sessionId: input.session_id,
|
|
15
|
+
toolName: input.tool_name,
|
|
16
|
+
});
|
|
17
|
+
// Set trace context so all spans within this hook invocation correlate.
|
|
18
|
+
client.tracer.setContext({
|
|
19
|
+
traceId: (0, argus_1.deriveTraceId)(input.session_id),
|
|
20
|
+
sessionId: input.session_id,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
switch (event) {
|
|
24
|
+
case "SessionStart":
|
|
25
|
+
return await handleSessionStart(input, client, deps);
|
|
26
|
+
case "SessionEnd":
|
|
27
|
+
case "Stop":
|
|
28
|
+
return await handleSessionEnd(input, client);
|
|
29
|
+
case "PreToolUse":
|
|
30
|
+
return await handlePreToolUse(input, client);
|
|
31
|
+
case "PostToolUse":
|
|
32
|
+
case "PostToolUseFailure":
|
|
33
|
+
return await handlePostToolUse(input, client);
|
|
34
|
+
case "UserPromptSubmit":
|
|
35
|
+
return await handleUserPromptSubmit(input, client);
|
|
36
|
+
case "BeforeReadFile":
|
|
37
|
+
case "AfterFileEdit":
|
|
38
|
+
case "BeforeShellExecution":
|
|
39
|
+
case "AfterShellExecution":
|
|
40
|
+
return await handleTraceOnly(input, client);
|
|
41
|
+
default:
|
|
42
|
+
client.logger.debug("hook.passthrough", { event });
|
|
43
|
+
client.tracer.record("hook.passthrough", "skipped", {
|
|
44
|
+
attributes: { event },
|
|
45
|
+
});
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
client.tracer.clearContext();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── SessionStart ───────────────────────────────────────────────────
|
|
54
|
+
async function handleSessionStart(input, client, deps) {
|
|
55
|
+
client.logger.info("lifecycle.session_start", {
|
|
56
|
+
sessionId: input.session_id,
|
|
57
|
+
cwd: input.working_dir,
|
|
58
|
+
});
|
|
59
|
+
client.tracer.record("session.start", "ok", {});
|
|
60
|
+
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
61
|
+
const decision = await userGate(client, {
|
|
62
|
+
binName: "ory-goose",
|
|
63
|
+
harness: "goose",
|
|
64
|
+
// Goose's PreToolUse carries exit code 2; SessionStart is a subprocess
|
|
65
|
+
// hook too, so a declined login can hard-block the session.
|
|
66
|
+
allowBlock: true,
|
|
67
|
+
});
|
|
68
|
+
// Resolve the agent identity (machine credentials). Never blocks;
|
|
69
|
+
// attaches 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: "goose" });
|
|
72
|
+
// User → agent delegation tuple (best-effort, audit-trail only).
|
|
73
|
+
await recordUserDelegatesAgent(client);
|
|
74
|
+
if (!decision.proceed) {
|
|
75
|
+
return { action: "block", message: decision.reason };
|
|
76
|
+
}
|
|
77
|
+
if (decision.mode !== "disabled") {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
const resolved = (0, argus_1.resolveConfig)();
|
|
81
|
+
if (resolved.auditOnly) {
|
|
82
|
+
client.logger.info("config.audit_only", {
|
|
83
|
+
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
84
|
+
});
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
if (!resolved.projectUrl) {
|
|
88
|
+
client.logger.warn("config.not_configured", {
|
|
89
|
+
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
90
|
+
"Run 'npx ory-goose configure' to connect to an Ory project.",
|
|
91
|
+
});
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
// Legacy pre-supplied token verification (backward compat).
|
|
95
|
+
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
96
|
+
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
97
|
+
if (sessionToken) {
|
|
98
|
+
await verifySessionToken(sessionToken, client);
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
if (oauth2Token) {
|
|
102
|
+
await verifyOAuth2Token(oauth2Token, client);
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
client.logger.warn("session.no_credentials", {
|
|
106
|
+
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
107
|
+
"Skipping authentication.",
|
|
108
|
+
});
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
async function verifySessionToken(token, client) {
|
|
112
|
+
try {
|
|
113
|
+
const session = await client.verifySession(token);
|
|
114
|
+
if (!session.active) {
|
|
115
|
+
client.logger.warn("session.inactive", {
|
|
116
|
+
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const oryErr = err;
|
|
122
|
+
client.logger.warn("session.verify_failed", {
|
|
123
|
+
code: oryErr.code,
|
|
124
|
+
message: oryErr.message,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function verifyOAuth2Token(token, client) {
|
|
129
|
+
try {
|
|
130
|
+
const tokenInfo = await client.introspectToken(token);
|
|
131
|
+
if (!tokenInfo.active) {
|
|
132
|
+
client.logger.warn("oauth2.token_inactive", {
|
|
133
|
+
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
client.logger.info("oauth2.session_authenticated", {
|
|
138
|
+
clientId: tokenInfo.clientId,
|
|
139
|
+
subject: tokenInfo.subject,
|
|
140
|
+
scope: tokenInfo.scope,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const oryErr = err;
|
|
145
|
+
client.logger.warn("oauth2.introspect_failed", {
|
|
146
|
+
code: oryErr.code,
|
|
147
|
+
message: oryErr.message,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ─── PreToolUse ────────────────────────────────────────────────────
|
|
152
|
+
async function handlePreToolUse(input, client) {
|
|
153
|
+
const toolName = input.tool_name ?? "unknown";
|
|
154
|
+
client.logger.info("lifecycle.pre_tool_use", {
|
|
155
|
+
sessionId: input.session_id,
|
|
156
|
+
toolName,
|
|
157
|
+
toolInput: input.tool_input,
|
|
158
|
+
});
|
|
159
|
+
const inputSummary = (0, argus_1.summarizeToolInput)(toolName, input.tool_input);
|
|
160
|
+
// In audit-only mode, log the invocation but skip permission checks.
|
|
161
|
+
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
162
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
163
|
+
attributes: { toolName, ...inputSummary },
|
|
164
|
+
});
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
168
|
+
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
169
|
+
const namespace = resolveNamespace();
|
|
170
|
+
try {
|
|
171
|
+
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
172
|
+
harness: "goose",
|
|
173
|
+
toolName,
|
|
174
|
+
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
175
|
+
spanAttributes: { toolName },
|
|
176
|
+
});
|
|
177
|
+
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
178
|
+
// user.interaction span is already recorded; pass through.
|
|
179
|
+
if (outcome.kind === "interactive") {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
const decision = outcome;
|
|
183
|
+
if (decision.kind === "fail_open") {
|
|
184
|
+
return handlePermissionError(decision.error, toolName, client);
|
|
185
|
+
}
|
|
186
|
+
const attrs = { toolName, ...inputSummary };
|
|
187
|
+
const decisionAttrs = decision.spanAttributes;
|
|
188
|
+
if (decision.kind === "allow") {
|
|
189
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
190
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
191
|
+
});
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
if (decision.kind === "observe") {
|
|
195
|
+
client.tracer.record("tool.block", "denied", {
|
|
196
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
197
|
+
});
|
|
198
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
199
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
200
|
+
});
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
client.tracer.record("tool.block", "denied", {
|
|
204
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
action: "block",
|
|
208
|
+
message: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
return handlePermissionError(err, toolName, client);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ─── PostToolUse / PostToolUseFailure ──────────────────────────────
|
|
216
|
+
async function handlePostToolUse(input, client) {
|
|
217
|
+
const toolName = input.tool_name ?? "unknown";
|
|
218
|
+
client.logger.info("lifecycle.post_tool_use", {
|
|
219
|
+
sessionId: input.session_id,
|
|
220
|
+
toolName,
|
|
221
|
+
error: input.error,
|
|
222
|
+
});
|
|
223
|
+
const status = input.event === "PostToolUseFailure" ? "error" : "ok";
|
|
224
|
+
client.tracer.record("tool.complete", status, {
|
|
225
|
+
attributes: {
|
|
226
|
+
toolName,
|
|
227
|
+
...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
|
|
228
|
+
...(0, argus_1.summarizeToolOutput)(toolName, input.tool_output),
|
|
229
|
+
...(input.error ? { error: input.error } : {}),
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
// ─── SessionEnd / Stop ─────────────────────────────────────────────
|
|
235
|
+
async function handleSessionEnd(input, client) {
|
|
236
|
+
client.logger.info("lifecycle.session_end", {
|
|
237
|
+
sessionId: input.session_id,
|
|
238
|
+
event: input.event,
|
|
239
|
+
});
|
|
240
|
+
client.tracer.record("session.end", "ok", {
|
|
241
|
+
attributes: { event: input.event },
|
|
242
|
+
});
|
|
243
|
+
return {};
|
|
244
|
+
}
|
|
245
|
+
// ─── UserPromptSubmit ──────────────────────────────────────────────
|
|
246
|
+
//
|
|
247
|
+
// User-facing phase. The human is the decision-maker; record an audit
|
|
248
|
+
// span only and never block.
|
|
249
|
+
async function handleUserPromptSubmit(input, client) {
|
|
250
|
+
client.logger.info("lifecycle.user_prompt_submit", {
|
|
251
|
+
sessionId: input.session_id,
|
|
252
|
+
});
|
|
253
|
+
client.tracer.record("user.prompt", "ok", {
|
|
254
|
+
attributes: { event: input.event },
|
|
255
|
+
});
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
// ─── Trace-only file / shell observation events ────────────────────
|
|
259
|
+
//
|
|
260
|
+
// BeforeReadFile, AfterFileEdit, BeforeShellExecution, AfterShellExecution
|
|
261
|
+
// are fine-grained observability hooks. The PreToolUse gate already
|
|
262
|
+
// authorizes the tool that drives them, so these are audit spans only.
|
|
263
|
+
async function handleTraceOnly(input, client) {
|
|
264
|
+
client.logger.info("lifecycle.observe", {
|
|
265
|
+
sessionId: input.session_id,
|
|
266
|
+
event: input.event,
|
|
267
|
+
filePath: input.file_path,
|
|
268
|
+
});
|
|
269
|
+
client.tracer.record("notification", "ok", {
|
|
270
|
+
attributes: {
|
|
271
|
+
event: input.event,
|
|
272
|
+
...(input.file_path ? { filePath: input.file_path } : {}),
|
|
273
|
+
...(input.command ? { hasCommand: true } : {}),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
279
|
+
function resolveNamespace() {
|
|
280
|
+
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Write the user→agent delegation tuple. Idempotent and fail-open:
|
|
284
|
+
* requires both principal subjects to be populated; any error (including
|
|
285
|
+
* unconfigured projectUrl, which manifests as a network_error) is logged
|
|
286
|
+
* and swallowed. The actual permission enforcement is unchanged — this
|
|
287
|
+
* tuple is purely audit-trail data.
|
|
288
|
+
*/
|
|
289
|
+
async function recordUserDelegatesAgent(client) {
|
|
290
|
+
const user = client.userPrincipal.subject;
|
|
291
|
+
const agent = client.agentPrincipal.subject;
|
|
292
|
+
if (!user || !agent) {
|
|
293
|
+
client.logger.debug("delegation.skip", {
|
|
294
|
+
reason: "missing principal",
|
|
295
|
+
hasUser: !!user,
|
|
296
|
+
hasAgent: !!agent,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await client.createRelationship({
|
|
302
|
+
namespace: resolveNamespace(),
|
|
303
|
+
object: `agent:${agent}`,
|
|
304
|
+
relation: "delegate",
|
|
305
|
+
subjectId: `user:${user}`,
|
|
306
|
+
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
const oryErr = err;
|
|
310
|
+
client.logger.warn("delegation.user_to_agent.failed", {
|
|
311
|
+
code: oryErr.code,
|
|
312
|
+
message: oryErr.message,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function handlePermissionError(oryErr, toolName, client) {
|
|
317
|
+
if (oryErr.code === "network_error") {
|
|
318
|
+
client.logger.warn("permission.network_error", {
|
|
319
|
+
toolName,
|
|
320
|
+
message: "Ory unreachable, failing open",
|
|
321
|
+
});
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
if (oryErr.code === "rate_limited") {
|
|
325
|
+
client.logger.warn("permission.rate_limited", {
|
|
326
|
+
toolName,
|
|
327
|
+
message: "Ory rate limited, failing open",
|
|
328
|
+
});
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
// For other errors, also fail open but log prominently.
|
|
332
|
+
client.logger.error("permission.check.error", {
|
|
333
|
+
toolName,
|
|
334
|
+
code: oryErr.code,
|
|
335
|
+
message: oryErr.message,
|
|
336
|
+
});
|
|
337
|
+
return {};
|
|
338
|
+
}
|
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Goose hook entry point.
|
|
4
|
+
* This script is invoked by Goose for each hook event.
|
|
5
|
+
* Input: JSON on stdin
|
|
6
|
+
* Output: JSON on stdout (the `RawHookAction` shape — `{ action, message }`).
|
|
7
|
+
* A `{"action":"block"}` payload blocks the action (PreToolUse only); we also
|
|
8
|
+
* exit with code 2 as a defensive secondary block signal.
|
|
9
|
+
*
|
|
10
|
+
* Verified against goose v1.39.0.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Goose hook entry point.
|
|
5
|
+
* This script is invoked by Goose for each hook event.
|
|
6
|
+
* Input: JSON on stdin
|
|
7
|
+
* Output: JSON on stdout (the `RawHookAction` shape — `{ action, message }`).
|
|
8
|
+
* A `{"action":"block"}` payload blocks the action (PreToolUse only); we also
|
|
9
|
+
* exit with code 2 as a defensive secondary block signal.
|
|
10
|
+
*
|
|
11
|
+
* Verified against goose v1.39.0.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const argus_1 = require("@ory/argus");
|
|
15
|
+
const handlers_js_1 = require("./handlers.js");
|
|
16
|
+
/**
|
|
17
|
+
* Read all of stdin as a string.
|
|
18
|
+
*
|
|
19
|
+
* Uses event listeners instead of `for await` so that we don't depend on the
|
|
20
|
+
* parent process closing stdin (sending EOF). If no EOF arrives, we resolve
|
|
21
|
+
* after a short idle gap once data has been received.
|
|
22
|
+
*/
|
|
23
|
+
function readStdin() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let resolved = false;
|
|
27
|
+
function done() {
|
|
28
|
+
if (!resolved) {
|
|
29
|
+
resolved = true;
|
|
30
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
process.stdin.on("data", (chunk) => {
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
clearTimeout(idleTimer);
|
|
36
|
+
idleTimer = setTimeout(done, 100);
|
|
37
|
+
});
|
|
38
|
+
process.stdin.on("end", done);
|
|
39
|
+
process.stdin.on("error", (err) => {
|
|
40
|
+
if (!resolved) {
|
|
41
|
+
resolved = true;
|
|
42
|
+
reject(err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
let idleTimer;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const client = argus_1.OryAgentClient.fromEnv("goose");
|
|
50
|
+
const raw = await readStdin();
|
|
51
|
+
let input;
|
|
52
|
+
try {
|
|
53
|
+
input = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
57
|
+
await client.tracer.shutdown();
|
|
58
|
+
process.exit(0); // Don't block on parse errors
|
|
59
|
+
}
|
|
60
|
+
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
61
|
+
if (output.action === "block") {
|
|
62
|
+
// Goose stops execution on the first explicit deny. The structured block
|
|
63
|
+
// signal is the `action` key; emit it on stdout and exit 2 as a defensive
|
|
64
|
+
// secondary signal — both indicate a hard deny.
|
|
65
|
+
process.stdout.write(JSON.stringify(output));
|
|
66
|
+
await client.tracer.shutdown();
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
await client.tracer.shutdown();
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
main().catch((err) => {
|
|
73
|
+
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
74
|
+
process.exit(0); // Fail open
|
|
75
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleHookEvent = void 0;
|
|
4
|
+
var handlers_js_1 = require("./handlers.js");
|
|
5
|
+
Object.defineProperty(exports, "handleHookEvent", { enumerable: true, get: function () { return handlers_js_1.handleHookEvent; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goose hook event payload delivered on stdin.
|
|
3
|
+
*
|
|
4
|
+
* Goose (Block's open-source agent CLI) invokes external shell hooks as
|
|
5
|
+
* child processes, passing JSON on stdin and reading JSON from stdout. For
|
|
6
|
+
* PreToolUse, a `{"action":"block"}` stdout payload blocks the action; any
|
|
7
|
+
* other action allows it (fail-open). Exit code 2 is honored as a defensive
|
|
8
|
+
* secondary block signal.
|
|
9
|
+
*
|
|
10
|
+
* Events: SessionStart, SessionEnd, Stop, UserPromptSubmit, PreToolUse,
|
|
11
|
+
* PostToolUse, PostToolUseFailure, BeforeReadFile, AfterFileEdit,
|
|
12
|
+
* BeforeShellExecution, AfterShellExecution.
|
|
13
|
+
*
|
|
14
|
+
* Verified against goose v1.39.0. Hooks landed via PR #9304.
|
|
15
|
+
*/
|
|
16
|
+
export interface GooseHookInput {
|
|
17
|
+
/** Lifecycle event name, e.g. "SessionStart" / "PreToolUse". */
|
|
18
|
+
event: string;
|
|
19
|
+
/** Stable identifier for the Goose session. */
|
|
20
|
+
session_id: string;
|
|
21
|
+
/** Optional matcher context supplied by the binary. Not consumed. */
|
|
22
|
+
matcher_context?: Record<string, unknown>;
|
|
23
|
+
/** Working directory of the session. */
|
|
24
|
+
working_dir?: string;
|
|
25
|
+
tool_name?: string;
|
|
26
|
+
tool_input?: Record<string, unknown>;
|
|
27
|
+
tool_output?: Record<string, unknown>;
|
|
28
|
+
/** Present on PostToolUseFailure. */
|
|
29
|
+
error?: string;
|
|
30
|
+
prompt?: string;
|
|
31
|
+
file_path?: string;
|
|
32
|
+
command?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Goose hook output written to stdout (the `RawHookAction` shape).
|
|
36
|
+
*
|
|
37
|
+
* Only PreToolUse honors a block action. We emit `{ action: "block",
|
|
38
|
+
* message }` and pair it with exit code 2 as a defensive secondary block
|
|
39
|
+
* signal in the hook entry point.
|
|
40
|
+
*
|
|
41
|
+
* Verified against goose v1.39.0: the JSON key is `action` with values
|
|
42
|
+
* `allow` / `block` / `warn`, and the human-readable text field is `message`.
|
|
43
|
+
*/
|
|
44
|
+
export interface GooseHookOutput {
|
|
45
|
+
action?: "allow" | "block" | "warn";
|
|
46
|
+
message?: string;
|
|
47
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ory/goose",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Ory plugin for Goose (Block's open-source agent CLI): scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://ory.com",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"ory",
|
|
9
|
+
"goose",
|
|
10
|
+
"goose-cli",
|
|
11
|
+
"block",
|
|
12
|
+
"hooks",
|
|
13
|
+
"plugin",
|
|
14
|
+
"identity",
|
|
15
|
+
"identity-management",
|
|
16
|
+
"iam",
|
|
17
|
+
"authentication",
|
|
18
|
+
"authorization",
|
|
19
|
+
"access-control",
|
|
20
|
+
"permissions",
|
|
21
|
+
"rbac",
|
|
22
|
+
"zanzibar",
|
|
23
|
+
"oauth",
|
|
24
|
+
"oauth2",
|
|
25
|
+
"openid-connect",
|
|
26
|
+
"oidc",
|
|
27
|
+
"session",
|
|
28
|
+
"mfa",
|
|
29
|
+
"sso",
|
|
30
|
+
"audit",
|
|
31
|
+
"audit-log",
|
|
32
|
+
"compliance",
|
|
33
|
+
"agent",
|
|
34
|
+
"ai-agent",
|
|
35
|
+
"agent-security",
|
|
36
|
+
"guardrails",
|
|
37
|
+
"llm",
|
|
38
|
+
"tracing",
|
|
39
|
+
"distributed-tracing",
|
|
40
|
+
"observability",
|
|
41
|
+
"kratos",
|
|
42
|
+
"keto",
|
|
43
|
+
"hydra"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/",
|
|
48
|
+
"provenance": true
|
|
49
|
+
},
|
|
50
|
+
"main": "dist/index.js",
|
|
51
|
+
"types": "dist/index.d.ts",
|
|
52
|
+
"exports": {
|
|
53
|
+
".": {
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
55
|
+
"default": "./dist/index.js"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"bin": {
|
|
59
|
+
"ory-goose": "dist/cli/main.js",
|
|
60
|
+
"ory-goose-hook": "dist/hook.js",
|
|
61
|
+
"ory-goose-setup": "dist/cli/setup.js"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"dist",
|
|
65
|
+
"!dist/dev",
|
|
66
|
+
"!dist/**/*.tsbuildinfo"
|
|
67
|
+
],
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@ory/argus": "0.10.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"typescript": "^6.0.2",
|
|
73
|
+
"vitest": "4.1.4"
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=22"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsc",
|
|
80
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"test:watch": "vitest",
|
|
83
|
+
"typecheck": "tsc --noEmit",
|
|
84
|
+
"dev": "node dist/dev/launcher.js"
|
|
85
|
+
}
|
|
86
|
+
}
|