@ory/continue 0.13.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -61
- package/dist/cli/main.js +17 -8
- package/dist/cli/setup.js +29 -6
- package/dist/handlers.d.ts +2 -10
- package/dist/handlers.js +145 -394
- package/dist/hook.js +15 -5
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Security and developer experience for [Continue](https://continue.dev)'s `cn` CLI, powered by [Ory](https://ory.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Continue runs real actions on your machine — editing files, running shell commands, calling APIs. This plugin gives those actions an identity, a permission check, and an audit trail.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
One command installs two independent halves:
|
|
8
|
+
|
|
9
|
+
- **Developer experience** — the Ory skill catalog, the local Ory dev stack, 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
|
This targets the **`cn` CLI** (not the Continue IDE extension), verified against `@continuedev/cli`.
|
|
10
13
|
|
|
@@ -17,7 +20,7 @@ This targets the **`cn` CLI** (not the Continue IDE extension), verified against
|
|
|
17
20
|
|
|
18
21
|
## Get started
|
|
19
22
|
|
|
20
|
-
Run one command
|
|
23
|
+
Run one command:
|
|
21
24
|
|
|
22
25
|
```bash
|
|
23
26
|
npx -y -p @ory/continue ory-cn install
|
|
@@ -25,29 +28,53 @@ npx -y -p @ory/continue ory-cn install
|
|
|
25
28
|
|
|
26
29
|
Continue has no native install command, so this writes the Ory hooks into your project settings and drops the Ory skills into your project — Continue turns each skill into a slash command automatically (so the local Ory commands show up as `/ory-local-up`, `/ory-local-down`, and `/ory-temporal-up`).
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- **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.
|
|
31
|
-
- **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
|
|
32
|
-
- **Audit-only** — skip Ory entirely and just log what Continue does.
|
|
33
|
-
|
|
34
|
-
That's it. Confirm everything landed with:
|
|
31
|
+
Confirm everything landed with:
|
|
35
32
|
|
|
36
33
|
```bash
|
|
37
34
|
npx -y -p @ory/continue ory-cn status
|
|
38
35
|
```
|
|
39
36
|
|
|
40
|
-
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, and recent activity.
|
|
37
|
+
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, and recent activity. Until you connect Agent Security, the identity and permission rows say so and name what's missing.
|
|
38
|
+
|
|
39
|
+
## Skills and commands
|
|
40
|
+
|
|
41
|
+
Installing the plugin drops the full Ory playbook catalog into Continue. **Skills** are model-invoked — say what you want in plain language. Continue also surfaces every skill as a slash command, so `/ory-auth-setup` works too.
|
|
42
|
+
|
|
43
|
+
| Skill | What it does for you |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
|
|
46
|
+
| `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
|
|
47
|
+
| `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
|
|
48
|
+
| `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
|
|
49
|
+
| `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
|
|
50
|
+
| `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
|
|
51
|
+
| `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
|
|
52
|
+
| `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
|
|
53
|
+
| `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
|
|
54
|
+
| `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
|
|
55
|
+
|
|
56
|
+
The local-stack playbooks install as skills too, so they show up as slash commands:
|
|
57
|
+
|
|
58
|
+
| Command | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `/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 |
|
|
61
|
+
| `/ory-local-down` | Stops it, keeping your data volumes |
|
|
62
|
+
| `/ory-temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
|
|
63
|
+
|
|
64
|
+
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`.
|
|
41
65
|
|
|
42
|
-
|
|
66
|
+
A built-in **Ory MCP server** rounds it out — Continue can manage identities, projects, and permissions straight from chat.
|
|
67
|
+
|
|
68
|
+
So: ask Continue *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
|
|
43
69
|
|
|
44
70
|
## What you get
|
|
45
71
|
|
|
46
|
-
|
|
72
|
+
Out of the box, every tool Continue runs produces a privacy-safe structured activity event in the unified local log.
|
|
73
|
+
|
|
74
|
+
Once you connect to Ory Agent Security, two more things happen automatically:
|
|
47
75
|
|
|
48
|
-
- **Who's driving.** You sign in once in your browser (
|
|
49
|
-
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **
|
|
50
|
-
- **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.
|
|
76
|
+
- **Who's driving.** You sign in once in your browser; Continue (and any sub-agents it spawns) each get their own identity. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later.
|
|
77
|
+
- **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.
|
|
51
78
|
|
|
52
79
|
If Ory is ever unreachable, the plugin gets out of the way and lets Continue keep working — so it can't lock you out.
|
|
53
80
|
|
|
@@ -55,80 +82,63 @@ Under the hood, Continue runs the plugin the same way [Claude Code](https://gith
|
|
|
55
82
|
|
|
56
83
|
### See what's happening
|
|
57
84
|
|
|
58
|
-
Everything the plugin does is observable out of the box
|
|
85
|
+
Everything the plugin does is observable out of the box:
|
|
59
86
|
|
|
60
|
-
- **
|
|
61
|
-
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
87
|
+
- **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/continue/ory-agent-debug.log`. View events, decisions, and errors live with:
|
|
62
88
|
|
|
63
89
|
```bash
|
|
64
90
|
npx -y -p @ory/continue ory-cn watch
|
|
65
91
|
```
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
- **
|
|
93
|
+
Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
|
|
94
|
+
- **Live debug.** Launch Continue 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.
|
|
69
95
|
|
|
70
96
|
### Ready to enforce?
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
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.
|
|
73
99
|
|
|
74
100
|
```bash
|
|
75
|
-
npx -y -p @ory/continue ory-cn permissions
|
|
101
|
+
npx -y -p @ory/continue ory-cn permissions # what the project grants, and the live mode
|
|
76
102
|
```
|
|
77
103
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
## Also: add login to your own app
|
|
81
|
-
|
|
82
|
-
Beyond securing Continue, the plugin helps you build Ory into whatever you're working on. Ask Continue *"add Ory login to this app"* and it scaffolds the login, registration, recovery, and settings pages (using [Ory Elements](https://github.com/ory/elements)) wired to a local Ory — no signup or keys needed. Start that local Ory with `/ory-local-up` (it prints a test email + password to sign in with) and tear it down with `/ory-local-down`.
|
|
104
|
+
Then a denied tool is actually blocked and Continue shows why.
|
|
83
105
|
|
|
84
|
-
|
|
106
|
+
## Connect to Ory Agent Security
|
|
85
107
|
|
|
86
|
-
|
|
108
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
87
109
|
|
|
88
|
-
|
|
110
|
+
| Value | Flag | Environment variable |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Project URL | `--project-url` | `ORY_PROJECT_URL` |
|
|
113
|
+
| Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
|
|
114
|
+
| Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
|
|
89
115
|
|
|
90
116
|
```bash
|
|
91
117
|
npx -y -p @ory/continue ory-cn configure \
|
|
92
118
|
--project-url https://<slug>.projects.oryapis.com \
|
|
93
|
-
--
|
|
94
|
-
--user-login
|
|
119
|
+
--agent-security-url https://agents.console.ory.com
|
|
95
120
|
```
|
|
96
121
|
|
|
97
|
-
|
|
122
|
+
`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`.
|
|
98
123
|
|
|
99
|
-
|
|
100
|
-
<summary>Create the sign-in client by hand</summary>
|
|
101
|
-
|
|
102
|
-
The guided setup normally does this. To do it yourself, create a **public** OAuth2 client (no secret) listing all four loopback URLs — the plugin tries each in turn so sign-in survives a busy port:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
ory create oauth2-client --project <project-id> \
|
|
106
|
-
--name "ory-agent-plugin" \
|
|
107
|
-
--grant-type authorization_code,refresh_token \
|
|
108
|
-
--response-type code \
|
|
109
|
-
--scope openid,offline_access \
|
|
110
|
-
--token-endpoint-auth-method none \
|
|
111
|
-
--redirect-uri http://127.0.0.1:47823/callback \
|
|
112
|
-
--redirect-uri http://127.0.0.1:47824/callback \
|
|
113
|
-
--redirect-uri http://127.0.0.1:47825/callback \
|
|
114
|
-
--redirect-uri http://127.0.0.1:47826/callback
|
|
115
|
-
```
|
|
124
|
+
**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.
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
**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 Continue session registers its own identity automatically on first use.
|
|
118
127
|
|
|
119
|
-
|
|
128
|
+
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.
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
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.
|
|
122
131
|
|
|
123
132
|
## Commands
|
|
124
133
|
|
|
125
134
|
```
|
|
126
|
-
ory-cn install | uninstall
|
|
135
|
+
ory-cn install | uninstall Install (add --project-url to also connect Agent Security) / remove
|
|
127
136
|
ory-cn status Show configuration, identities, permission coverage, recent activity
|
|
128
|
-
ory-cn
|
|
129
|
-
ory-cn
|
|
130
|
-
ory-cn configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --user-login, --audit-only)
|
|
137
|
+
ory-cn permissions <cmd> status (read-only; grants + posture live in the Ory Console)
|
|
138
|
+
ory-cn configure <flags> Connect a project (--project-url) or --disconnect
|
|
131
139
|
ory-cn agent <status|unregister> Manage Continue's own auto-created identity
|
|
140
|
+
ory-cn local <up|down|status|…> Run / manage a local Ory in Docker
|
|
141
|
+
ory-cn version Print plugin, core, and Node versions (--json for machine-readable)
|
|
132
142
|
```
|
|
133
143
|
|
|
134
144
|
All prefixed with `npx -y -p @ory/continue`.
|
|
@@ -136,10 +146,9 @@ All prefixed with `npx -y -p @ory/continue`.
|
|
|
136
146
|
## Troubleshooting
|
|
137
147
|
|
|
138
148
|
- **`/ory-local-up` fails** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
139
|
-
- **Browser sign-in loops** — reset with `ory-cn agent unregister` and try again.
|
|
140
|
-
- **
|
|
149
|
+
- **Browser sign-in loops** (after connecting) — reset with `ory-cn agent unregister` and try again.
|
|
150
|
+
- **Running an older CLI than expected** — `npx -p @ory/continue` (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/continue ory-cn version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/continue ory-cn install`. Pinning an exact version (`@ory/continue@<version>`) also bypasses the cached copy.
|
|
141
151
|
- **Hooks stuck on an old version** — re-run the installer.
|
|
142
|
-
- **Want to see what's happening** — `npx -y -p @ory/continue ory-cn status` for a snapshot, `npx -y -p @ory/continue ory-cn 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/continue/` (see [See what's happening](#see-whats-happening)).
|
|
143
152
|
|
|
144
153
|
## Learn more
|
|
145
154
|
|
package/dist/cli/main.js
CHANGED
|
@@ -54,6 +54,11 @@ const argus_1 = require("@ory/argus");
|
|
|
54
54
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
55
55
|
function main() {
|
|
56
56
|
const [command, ...args] = process.argv.slice(2);
|
|
57
|
+
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
58
|
+
// Without this the switch below ignores the flag and `install --help`
|
|
59
|
+
// performs a real install (#221).
|
|
60
|
+
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
61
|
+
return help();
|
|
57
62
|
switch (command) {
|
|
58
63
|
case "install":
|
|
59
64
|
(0, argus_1.beginDeferNextSteps)();
|
|
@@ -65,7 +70,10 @@ function main() {
|
|
|
65
70
|
break;
|
|
66
71
|
case "uninstall":
|
|
67
72
|
uninstall(args);
|
|
68
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
73
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
74
|
+
harness: "continue",
|
|
75
|
+
purge: args.includes("--purge"),
|
|
76
|
+
}).then(() => process.exit(0), (err) => {
|
|
69
77
|
console.error(err.message ?? err);
|
|
70
78
|
process.exit(1);
|
|
71
79
|
});
|
|
@@ -74,7 +82,7 @@ function main() {
|
|
|
74
82
|
(0, argus_1.runConfigureCommand)("ory-cn", args);
|
|
75
83
|
break;
|
|
76
84
|
case "agent":
|
|
77
|
-
(0, argus_1.runAgentCommand)("ory-cn", args).then((code) => process.exit(code), (err) => {
|
|
85
|
+
(0, argus_1.runAgentCommand)("ory-cn", "continue", args).then((code) => process.exit(code), (err) => {
|
|
78
86
|
console.error(err.message ?? err);
|
|
79
87
|
process.exit(1);
|
|
80
88
|
});
|
|
@@ -94,15 +102,15 @@ function main() {
|
|
|
94
102
|
process.exit(1);
|
|
95
103
|
});
|
|
96
104
|
break;
|
|
105
|
+
case "watch":
|
|
106
|
+
(0, argus_1.runWatchCommand)("ory-cn", "continue", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
107
|
+
break;
|
|
97
108
|
case "local":
|
|
98
109
|
(0, argus_1.runLocalCommand)("ory-cn", args).catch((err) => {
|
|
99
110
|
console.error(err.message ?? err);
|
|
100
111
|
process.exit(1);
|
|
101
112
|
});
|
|
102
113
|
break;
|
|
103
|
-
case "watch":
|
|
104
|
-
(0, argus_1.runWatchCommand)("continue", args);
|
|
105
|
-
break;
|
|
106
114
|
case "version":
|
|
107
115
|
case "--version":
|
|
108
116
|
case "-v":
|
|
@@ -146,7 +154,7 @@ function printQuickstart() {
|
|
|
146
154
|
console.log("Quickstart:");
|
|
147
155
|
console.log(" 1. Inside Continue, run /local-up to start a local Ory stack.");
|
|
148
156
|
console.log(" 2. Ask Continue \"add Ory auth to this app\" to scaffold login/signup.");
|
|
149
|
-
console.log(" 3. Set
|
|
157
|
+
console.log(" 3. Set ORY_OAUTH2_CLIENT_ID so the per-session PKCE browser login can complete.");
|
|
150
158
|
}
|
|
151
159
|
async function status() {
|
|
152
160
|
await (0, argus_1.runStatusCommand)("ory-cn", "continue", {
|
|
@@ -170,10 +178,11 @@ Commands:
|
|
|
170
178
|
uninstall Remove the Ory hooks and skills
|
|
171
179
|
configure Set or view Ory project URL and API key
|
|
172
180
|
agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
|
|
173
|
-
permissions
|
|
181
|
+
permissions Show the live permission mode and per-tool coverage
|
|
174
182
|
setup [--project-dir] Write hooks directly to .continue/settings.json
|
|
175
183
|
status Show plugin status and configuration
|
|
176
|
-
watch [
|
|
184
|
+
watch [--json] [--lines <count>]
|
|
185
|
+
Follow the live activity/debug log
|
|
177
186
|
version Show version and the ory-agent-plugins build commit
|
|
178
187
|
local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
|
|
179
188
|
|
package/dist/cli/setup.js
CHANGED
|
@@ -56,11 +56,11 @@ const argus_1 = require("@ory/argus");
|
|
|
56
56
|
const assets_js_1 = require("./assets.js");
|
|
57
57
|
const BIN_NAME = "ory-cn-hook";
|
|
58
58
|
const PACKAGE_NAME = "@ory/continue";
|
|
59
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
59
60
|
function getSettingsPath(args) {
|
|
60
61
|
return path.join(args.projectDir, ".continue", "settings.json");
|
|
61
62
|
}
|
|
62
|
-
function generateHooksConfig() {
|
|
63
|
-
const cmd = (0, argus_1.resolveHookCommand)(PACKAGE_NAME, BIN_NAME);
|
|
63
|
+
function generateHooksConfig(cmd) {
|
|
64
64
|
return {
|
|
65
65
|
SessionStart: (0, argus_1.matcherHookEntry)(cmd),
|
|
66
66
|
PreToolUse: (0, argus_1.matcherHookEntry)(cmd),
|
|
@@ -81,9 +81,9 @@ function main() {
|
|
|
81
81
|
process.exit(0);
|
|
82
82
|
}
|
|
83
83
|
const args = (0, argus_1.parseSetupArgs)();
|
|
84
|
-
const hooksConfig = generateHooksConfig();
|
|
85
84
|
if (args.print) {
|
|
86
|
-
|
|
85
|
+
// Nothing is resolved or written on a --print run.
|
|
86
|
+
console.log(JSON.stringify({ hooks: generateHooksConfig('node "<runtime shim>"') }, null, 2));
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
const settingsPath = getSettingsPath(args);
|
|
@@ -101,10 +101,30 @@ function main() {
|
|
|
101
101
|
(0, assets_js_1.uninstallContinueOryAssets)(continueDir);
|
|
102
102
|
console.log(`Removed Ory hooks and MCP server from ${settingsPath}`);
|
|
103
103
|
console.log(`Removed Ory skills from ${continueDir}`);
|
|
104
|
+
(0, argus_1.removeRuntimeWiring)("continue");
|
|
105
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
106
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
107
|
+
}
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
110
|
+
const runtime = (0, argus_1.wireRuntime)({
|
|
111
|
+
harness: "continue",
|
|
112
|
+
packageName: PACKAGE_NAME,
|
|
113
|
+
packageRoot: PACKAGE_ROOT,
|
|
114
|
+
installCommand: "npx -y -p @ory/continue ory-cn install",
|
|
115
|
+
args: process.argv.slice(2),
|
|
116
|
+
});
|
|
117
|
+
console.log(runtime.target.kind === "linked"
|
|
118
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
119
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
120
|
+
for (const pruned of runtime.prunedStores) {
|
|
121
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
122
|
+
}
|
|
123
|
+
const hooksConfig = generateHooksConfig((0, argus_1.requireHookCommand)(runtime));
|
|
106
124
|
let merged = (0, argus_1.mergeMatcherHooks)(existing, hooksConfig, BIN_NAME);
|
|
107
|
-
|
|
125
|
+
if (runtime.mcpServer) {
|
|
126
|
+
merged = (0, argus_1.mergeMcpServer)(merged, runtime.mcpServer);
|
|
127
|
+
}
|
|
108
128
|
(0, argus_1.writeJsonFile)(settingsPath, merged);
|
|
109
129
|
(0, assets_js_1.installContinueOryAssets)(continueDir);
|
|
110
130
|
console.log(`Ory hooks and MCP server installed to ${settingsPath}`);
|
|
@@ -119,7 +139,10 @@ main();
|
|
|
119
139
|
// stored Ory credentials. When required by the plugin's main CLI, that
|
|
120
140
|
// command owns the purge, so the `require.main` guard prevents a double run.
|
|
121
141
|
if (require.main === module && process.argv.includes("--uninstall")) {
|
|
122
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
142
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
143
|
+
harness: "continue",
|
|
144
|
+
purge: process.argv.includes("--purge"),
|
|
145
|
+
}).then(() => process.exit(0), (err) => {
|
|
123
146
|
console.error(err.message ?? err);
|
|
124
147
|
process.exit(1);
|
|
125
148
|
});
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureAgentIdentity, ensureSubAgentIdentity, ensureUserAuthenticated } from "@ory/argus";
|
|
2
|
+
import type { OryAgentClient } from "@ory/argus";
|
|
2
3
|
import type { ContinueHookInput, ContinueHookOutput } from "./types.js";
|
|
3
4
|
export interface HandleHookEventDeps {
|
|
4
|
-
/** Test injection point for the user login flow. */
|
|
5
5
|
userLogin?: typeof ensureUserAuthenticated;
|
|
6
|
-
/** Test injection point for the agent identity gate. */
|
|
7
6
|
agentGate?: typeof ensureAgentIdentity;
|
|
8
|
-
/** Test injection point for the sub-agent identity resolver. */
|
|
9
7
|
subAgentGate?: typeof ensureSubAgentIdentity;
|
|
10
8
|
}
|
|
11
|
-
/**
|
|
12
|
-
* Route a Continue CLI hook event to the appropriate Ory integration.
|
|
13
|
-
*
|
|
14
|
-
* Event names are verified against @continuedev/cli. Unknown events pass
|
|
15
|
-
* through.
|
|
16
|
-
*/
|
|
17
9
|
export declare function handleHookEvent(input: ContinueHookInput, client: OryAgentClient, deps?: HandleHookEventDeps): Promise<ContinueHookOutput>;
|
package/dist/handlers.js
CHANGED
|
@@ -3,62 +3,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.handleHookEvent = handleHookEvent;
|
|
4
4
|
const argus_1 = require("@ory/argus");
|
|
5
5
|
const HARNESS = "continue";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Route a Continue CLI hook event to the appropriate Ory integration.
|
|
9
|
-
*
|
|
10
|
-
* Event names are verified against @continuedev/cli. Unknown events pass
|
|
11
|
-
* through.
|
|
12
|
-
*/
|
|
13
|
-
async function handleHookEvent(input, client, deps = {}) {
|
|
6
|
+
function handleHookEvent(input, client, deps = {}) {
|
|
14
7
|
const event = input.hook_event_name;
|
|
15
8
|
client.logger.debug("hook.received", {
|
|
16
9
|
event,
|
|
17
10
|
sessionId: input.session_id,
|
|
18
11
|
toolName: input.tool_name,
|
|
19
12
|
});
|
|
20
|
-
|
|
21
|
-
client.tracer.setContext({
|
|
22
|
-
traceId: (0, argus_1.deriveTraceId)(input.session_id),
|
|
23
|
-
sessionId: input.session_id,
|
|
24
|
-
});
|
|
25
|
-
try {
|
|
13
|
+
return (0, argus_1.withHookContext)(client, { sessionId: input.session_id }, async () => {
|
|
26
14
|
switch (event) {
|
|
27
15
|
case "SessionStart":
|
|
28
|
-
return
|
|
16
|
+
return handleSessionStart(input, client, deps);
|
|
29
17
|
case "PreToolUse":
|
|
30
|
-
return
|
|
18
|
+
return handlePreToolUse(input, client, deps);
|
|
31
19
|
case "PostToolUse":
|
|
32
|
-
return
|
|
20
|
+
return handlePostToolUse(input, client);
|
|
33
21
|
case "PostToolUseFailure":
|
|
34
|
-
return
|
|
22
|
+
return handlePostToolUseFailure(input, client);
|
|
35
23
|
case "PermissionRequest":
|
|
36
|
-
return
|
|
24
|
+
return handlePermissionRequest(input, client);
|
|
37
25
|
case "UserPromptSubmit":
|
|
38
|
-
return
|
|
26
|
+
return handleUserPromptSubmit(input, client);
|
|
39
27
|
case "SubagentStart":
|
|
40
|
-
return
|
|
28
|
+
return handleSubagentStart(input, client, deps);
|
|
41
29
|
case "SubagentStop":
|
|
42
|
-
return
|
|
30
|
+
return handleSubagentStop(input, client);
|
|
43
31
|
case "Stop":
|
|
44
|
-
return
|
|
32
|
+
return handleStop(input, client);
|
|
45
33
|
case "SessionEnd":
|
|
46
|
-
return
|
|
34
|
+
return handleSessionEnd(input, client);
|
|
47
35
|
case "PreCompact":
|
|
48
|
-
return
|
|
36
|
+
return handlePreCompact(input, client);
|
|
49
37
|
default:
|
|
50
38
|
client.logger.debug("hook.passthrough", { event });
|
|
51
|
-
client.
|
|
52
|
-
attributes: { event },
|
|
53
|
-
});
|
|
39
|
+
client.logger.activity("hook.passthrough", "skipped", { attributes: { event } });
|
|
54
40
|
return {};
|
|
55
41
|
}
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
client.tracer.clearContext();
|
|
59
|
-
}
|
|
42
|
+
});
|
|
60
43
|
}
|
|
61
|
-
// ─── SessionStart ───────────────────────────────────────────────────
|
|
62
44
|
async function handleSessionStart(input, client, deps) {
|
|
63
45
|
client.logger.info("lifecycle.session_start", {
|
|
64
46
|
sessionId: input.session_id,
|
|
@@ -66,183 +48,65 @@ async function handleSessionStart(input, client, deps) {
|
|
|
66
48
|
source: input.source,
|
|
67
49
|
cwd: input.cwd,
|
|
68
50
|
});
|
|
69
|
-
|
|
70
|
-
attributes: { model: input.model, source: input.source },
|
|
71
|
-
});
|
|
72
|
-
// Run the user login (interactive PKCE on first session, refresh when
|
|
73
|
-
// needed). Continue dispatches SessionStart as a subprocess hook that can
|
|
74
|
-
// hard-block via exit code 2, so allowBlock is true.
|
|
75
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
76
|
-
const decision = await userGate(client, {
|
|
77
|
-
binName: BIN_NAME,
|
|
51
|
+
await (0, argus_1.sessionStart)(client, {
|
|
78
52
|
harness: HARNESS,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Ory API calls.
|
|
84
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
85
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: HARNESS });
|
|
86
|
-
// Record the user→agent delegation for the audit trail. Best-effort,
|
|
87
|
-
// fail-open, and written at most once per install.
|
|
88
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
89
|
-
if (!decision.proceed) {
|
|
90
|
-
return { decision: "block", reason: decision.reason };
|
|
91
|
-
}
|
|
92
|
-
if (decision.mode !== "disabled") {
|
|
93
|
-
return {};
|
|
94
|
-
}
|
|
95
|
-
const resolved = (0, argus_1.resolveConfig)();
|
|
96
|
-
if (resolved.auditOnly) {
|
|
97
|
-
client.logger.info("config.audit_only", {
|
|
98
|
-
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
99
|
-
});
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
if (!resolved.projectUrl) {
|
|
103
|
-
client.logger.warn("config.not_configured", {
|
|
104
|
-
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
105
|
-
"Run 'npx ory-cn configure' to connect to an Ory project.",
|
|
106
|
-
});
|
|
107
|
-
return {};
|
|
108
|
-
}
|
|
109
|
-
// Try session token first, then OAuth2 token
|
|
110
|
-
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
111
|
-
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
112
|
-
if (sessionToken) {
|
|
113
|
-
await verifySessionToken(sessionToken, client);
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
|
-
if (oauth2Token) {
|
|
117
|
-
await verifyOAuth2Token(oauth2Token, client);
|
|
118
|
-
return {};
|
|
119
|
-
}
|
|
120
|
-
client.logger.warn("session.no_credentials", {
|
|
121
|
-
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
122
|
-
"Skipping authentication.",
|
|
53
|
+
binName: "ory-cn",
|
|
54
|
+
userLogin: deps.userLogin,
|
|
55
|
+
agentGate: deps.agentGate,
|
|
56
|
+
activityAttributes: { model: input.model, source: input.source },
|
|
123
57
|
});
|
|
124
58
|
return {};
|
|
125
59
|
}
|
|
126
|
-
async function
|
|
127
|
-
try {
|
|
128
|
-
const session = await client.verifySession(token);
|
|
129
|
-
if (!session.active) {
|
|
130
|
-
client.logger.warn("session.inactive", {
|
|
131
|
-
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
const oryErr = err;
|
|
137
|
-
client.logger.warn("session.verify_failed", {
|
|
138
|
-
code: oryErr.code,
|
|
139
|
-
message: oryErr.message,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async function verifyOAuth2Token(token, client) {
|
|
144
|
-
try {
|
|
145
|
-
const tokenInfo = await client.introspectToken(token);
|
|
146
|
-
if (!tokenInfo.active) {
|
|
147
|
-
client.logger.warn("oauth2.token_inactive", {
|
|
148
|
-
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
153
|
-
clientId: tokenInfo.clientId,
|
|
154
|
-
subject: tokenInfo.subject,
|
|
155
|
-
scope: tokenInfo.scope,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
const oryErr = err;
|
|
160
|
-
client.logger.warn("oauth2.introspect_failed", {
|
|
161
|
-
code: oryErr.code,
|
|
162
|
-
message: oryErr.message,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// ─── PreToolUse ─────────────────────────────────────────────────────
|
|
167
|
-
async function handlePreToolUse(input, client, deps = {}) {
|
|
60
|
+
async function handlePreToolUse(input, client, deps) {
|
|
168
61
|
const toolName = input.tool_name ?? "unknown";
|
|
169
62
|
client.logger.info("lifecycle.pre_tool_use", {
|
|
170
63
|
sessionId: input.session_id,
|
|
171
64
|
toolName,
|
|
172
65
|
toolInput: input.tool_input,
|
|
173
66
|
});
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// delegation. Best-effort — never blocks the actual tool call.
|
|
178
|
-
await maybeRegisterSubAgent(toolName, input.tool_input, client, deps);
|
|
179
|
-
// In audit-only mode, log the invocation but skip permission checks
|
|
180
|
-
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
181
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
182
|
-
attributes: { toolName, ...inputSummary },
|
|
183
|
-
});
|
|
184
|
-
return {};
|
|
185
|
-
}
|
|
186
|
-
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
187
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
188
|
-
const namespace = resolveNamespace();
|
|
189
|
-
try {
|
|
190
|
-
// No Continue-specific MCP tool parser exists in core, so gate every tool
|
|
191
|
-
// by name. MCP tools (if Continue namespaces them) flow through the same
|
|
192
|
-
// path keyed on the tool name.
|
|
193
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
67
|
+
const subAgentType = subagentType(toolName, input.tool_input);
|
|
68
|
+
if (subAgentType) {
|
|
69
|
+
await (0, argus_1.registerSubagent)(client, {
|
|
194
70
|
harness: HARNESS,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
// Interactive tools surface UI to the user and aren't external-system tool
|
|
200
|
-
// executions. gateToolCall has already recorded the `user.interaction`
|
|
201
|
-
// audit span; pass through with no tool.invoke/tool.block.
|
|
202
|
-
if (outcome.kind === "interactive") {
|
|
203
|
-
return {};
|
|
204
|
-
}
|
|
205
|
-
const decision = outcome;
|
|
206
|
-
if (decision.kind === "fail_open") {
|
|
207
|
-
return handlePermissionError(decision.error, toolName, client);
|
|
208
|
-
}
|
|
209
|
-
const attrs = { toolName, ...inputSummary };
|
|
210
|
-
const decisionAttrs = decision.spanAttributes;
|
|
211
|
-
if (decision.kind === "allow") {
|
|
212
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
213
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
214
|
-
});
|
|
215
|
-
return {};
|
|
216
|
-
}
|
|
217
|
-
if (decision.kind === "observe") {
|
|
218
|
-
client.tracer.record("tool.block", "denied", {
|
|
219
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
220
|
-
});
|
|
221
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
222
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
223
|
-
});
|
|
224
|
-
return {};
|
|
225
|
-
}
|
|
226
|
-
// deny
|
|
227
|
-
client.tracer.record("tool.block", "denied", {
|
|
228
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
71
|
+
subAgentType,
|
|
72
|
+
perSpawnId: input.tool_use_id,
|
|
73
|
+
subAgentGate: deps.subAgentGate,
|
|
229
74
|
});
|
|
230
|
-
const reason = (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace });
|
|
231
|
-
return {
|
|
232
|
-
decision: "block",
|
|
233
|
-
reason,
|
|
234
|
-
hookSpecificOutput: {
|
|
235
|
-
hookEventName: "PreToolUse",
|
|
236
|
-
permissionDecision: "deny",
|
|
237
|
-
permissionDecisionReason: reason,
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
return handlePermissionError(err, toolName, client);
|
|
243
75
|
}
|
|
76
|
+
const result = await (0, argus_1.gate)(client, {
|
|
77
|
+
harness: HARNESS,
|
|
78
|
+
toolName,
|
|
79
|
+
toolArgs: input.tool_input,
|
|
80
|
+
subjectFallback: `session:${input.session_id}`,
|
|
81
|
+
principals: actingSubagentPrincipals(input),
|
|
82
|
+
});
|
|
83
|
+
if (!result.blocked)
|
|
84
|
+
return {};
|
|
85
|
+
const reason = result.denialMessage ?? "Ory permission denied";
|
|
86
|
+
return {
|
|
87
|
+
decision: "block",
|
|
88
|
+
reason,
|
|
89
|
+
hookSpecificOutput: {
|
|
90
|
+
hookEventName: "PreToolUse",
|
|
91
|
+
permissionDecision: "deny",
|
|
92
|
+
permissionDecisionReason: reason,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function actingSubagentPrincipals(input) {
|
|
97
|
+
const type = input.agent_type;
|
|
98
|
+
if (!input.agent_id || !type)
|
|
99
|
+
return undefined;
|
|
100
|
+
const credential = (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, input.session_id, type);
|
|
101
|
+
if (!credential)
|
|
102
|
+
return undefined;
|
|
103
|
+
return {
|
|
104
|
+
subAgentClientId: credential.clientId,
|
|
105
|
+
subAgentType: type,
|
|
106
|
+
perSpawnId: input.agent_id,
|
|
107
|
+
sessionId: input.session_id,
|
|
108
|
+
};
|
|
244
109
|
}
|
|
245
|
-
// ─── PostToolUse ────────────────────────────────────────────────────
|
|
246
110
|
async function handlePostToolUse(input, client) {
|
|
247
111
|
const toolName = input.tool_name ?? "unknown";
|
|
248
112
|
client.logger.info("lifecycle.post_tool_use", {
|
|
@@ -250,17 +114,14 @@ async function handlePostToolUse(input, client) {
|
|
|
250
114
|
toolName,
|
|
251
115
|
toolResponse: input.tool_response,
|
|
252
116
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
|
|
259
|
-
},
|
|
117
|
+
(0, argus_1.complete)(client, {
|
|
118
|
+
toolName,
|
|
119
|
+
input: input.tool_input,
|
|
120
|
+
output: input.tool_response,
|
|
121
|
+
extraActivityAttributes: { toolUseId: input.tool_use_id },
|
|
260
122
|
});
|
|
261
123
|
return {};
|
|
262
124
|
}
|
|
263
|
-
// ─── PostToolUseFailure ─────────────────────────────────────────────
|
|
264
125
|
async function handlePostToolUseFailure(input, client) {
|
|
265
126
|
const toolName = input.tool_name ?? "unknown";
|
|
266
127
|
client.logger.info("lifecycle.post_tool_use_failure", {
|
|
@@ -268,252 +129,142 @@ async function handlePostToolUseFailure(input, client) {
|
|
|
268
129
|
toolName,
|
|
269
130
|
toolUseId: input.tool_use_id,
|
|
270
131
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
? input.error.slice(0, 500)
|
|
277
|
-
: undefined,
|
|
278
|
-
...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
|
|
279
|
-
},
|
|
132
|
+
(0, argus_1.fail)(client, {
|
|
133
|
+
toolName,
|
|
134
|
+
input: input.tool_input,
|
|
135
|
+
error: input.error,
|
|
136
|
+
extraActivityAttributes: { toolUseId: input.tool_use_id },
|
|
280
137
|
});
|
|
281
138
|
return {};
|
|
282
139
|
}
|
|
283
|
-
|
|
140
|
+
async function handlePermissionRequest(input, client) {
|
|
141
|
+
const toolName = input.tool_name ?? "unknown";
|
|
142
|
+
client.logger.info("lifecycle.permission_request", {
|
|
143
|
+
sessionId: input.session_id,
|
|
144
|
+
toolName,
|
|
145
|
+
toolInput: input.tool_input,
|
|
146
|
+
});
|
|
147
|
+
const result = await (0, argus_1.decideTool)(client, {
|
|
148
|
+
harness: HARNESS,
|
|
149
|
+
toolName,
|
|
150
|
+
toolArgs: input.tool_input,
|
|
151
|
+
subjectFallback: `session:${input.session_id}`,
|
|
152
|
+
});
|
|
153
|
+
if (result.kind === "not_connected") {
|
|
154
|
+
return permissionRequestFallback("Ory Agent Security not connected — deferring to user");
|
|
155
|
+
}
|
|
156
|
+
if (result.kind === "interactive") {
|
|
157
|
+
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
158
|
+
}
|
|
159
|
+
if (result.kind === "fail_open") {
|
|
160
|
+
return permissionRequestFallback(permissionFailureReason(result.decision));
|
|
161
|
+
}
|
|
162
|
+
if (result.kind === "deny") {
|
|
163
|
+
return permissionRequestDecision("deny", result.denialMessage ?? "Ory permission denied");
|
|
164
|
+
}
|
|
165
|
+
if (result.kind === "observe") {
|
|
166
|
+
return permissionRequestFallback("Ory observe mode — denied by policy, deferring to user");
|
|
167
|
+
}
|
|
168
|
+
return permissionRequestDecision("allow", "Ory permission granted");
|
|
169
|
+
}
|
|
170
|
+
function permissionRequestDecision(behavior, reason) {
|
|
171
|
+
return {
|
|
172
|
+
hookSpecificOutput: {
|
|
173
|
+
hookEventName: "PermissionRequest",
|
|
174
|
+
decision: { behavior },
|
|
175
|
+
message: reason,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function permissionRequestFallback(reason) {
|
|
180
|
+
return {
|
|
181
|
+
hookSpecificOutput: { hookEventName: "PermissionRequest", message: reason },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function permissionFailureReason(decision) {
|
|
185
|
+
if (decision.kind !== "fail_open")
|
|
186
|
+
return "Ory permission check failed";
|
|
187
|
+
const { code, message } = decision.error;
|
|
188
|
+
return code === "network_error" || code === "rate_limited"
|
|
189
|
+
? `Ory unavailable (${code}), falling back to user prompt`
|
|
190
|
+
: `Ory permission check failed: ${message}`;
|
|
191
|
+
}
|
|
284
192
|
async function handleUserPromptSubmit(input, client) {
|
|
285
193
|
client.logger.info("lifecycle.user_prompt_submit", {
|
|
286
194
|
sessionId: input.session_id,
|
|
287
195
|
promptLen: input.prompt?.length,
|
|
288
196
|
});
|
|
289
|
-
client.
|
|
197
|
+
client.logger.activity("user.prompt", "ok", {
|
|
290
198
|
attributes: { promptLen: input.prompt?.length },
|
|
291
199
|
});
|
|
292
200
|
return {};
|
|
293
201
|
}
|
|
294
|
-
// ─── SubagentStart ─────────────────────────────────────────────────
|
|
295
202
|
async function handleSubagentStart(input, client, deps) {
|
|
296
203
|
const subAgentType = input.subagent_type ?? input.agent_type;
|
|
297
|
-
const agentId = input.agent_id;
|
|
298
204
|
client.logger.info("lifecycle.subagent_start", {
|
|
299
205
|
sessionId: input.session_id,
|
|
300
206
|
subAgentType,
|
|
301
|
-
agentId,
|
|
207
|
+
agentId: input.agent_id,
|
|
302
208
|
});
|
|
303
|
-
client.
|
|
304
|
-
attributes: { subAgentType, agentId },
|
|
209
|
+
client.logger.activity("subagent.start", "ok", {
|
|
210
|
+
attributes: { subAgentType, agentId: input.agent_id },
|
|
305
211
|
});
|
|
306
212
|
if (subAgentType) {
|
|
307
|
-
await
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
213
|
+
await (0, argus_1.registerSubagent)(client, {
|
|
214
|
+
harness: HARNESS,
|
|
215
|
+
subAgentType,
|
|
216
|
+
perSpawnId: input.agent_id,
|
|
217
|
+
subAgentGate: deps.subAgentGate,
|
|
312
218
|
});
|
|
313
219
|
}
|
|
314
220
|
return {};
|
|
315
221
|
}
|
|
316
|
-
// ─── SubagentStop ──────────────────────────────────────────────────
|
|
317
222
|
async function handleSubagentStop(input, client) {
|
|
223
|
+
const subAgentType = input.subagent_type ?? input.agent_type;
|
|
318
224
|
client.logger.info("lifecycle.subagent_stop", {
|
|
319
225
|
sessionId: input.session_id,
|
|
320
|
-
subAgentType
|
|
226
|
+
subAgentType,
|
|
321
227
|
agentId: input.agent_id,
|
|
322
228
|
});
|
|
323
|
-
client.
|
|
229
|
+
client.logger.activity("subagent.stop", "ok", {
|
|
324
230
|
attributes: {
|
|
325
|
-
subAgentType
|
|
231
|
+
subAgentType,
|
|
326
232
|
agentId: input.agent_id,
|
|
327
233
|
responseLen: input.subagent_response?.length,
|
|
328
234
|
},
|
|
329
235
|
});
|
|
330
236
|
return {};
|
|
331
237
|
}
|
|
332
|
-
// ─── Stop ──────────────────────────────────────────────────────────
|
|
333
238
|
async function handleStop(input, client) {
|
|
334
|
-
client.logger.info("lifecycle.stop", {
|
|
335
|
-
|
|
336
|
-
});
|
|
337
|
-
client.tracer.record("turn.stop", "ok", {
|
|
239
|
+
client.logger.info("lifecycle.stop", { sessionId: input.session_id });
|
|
240
|
+
client.logger.activity("turn.stop", "ok", {
|
|
338
241
|
attributes: { responseLen: input.last_assistant_message?.length },
|
|
339
242
|
});
|
|
340
243
|
return {};
|
|
341
244
|
}
|
|
342
|
-
// ─── SessionEnd ────────────────────────────────────────────────────
|
|
343
245
|
async function handleSessionEnd(input, client) {
|
|
344
246
|
client.logger.info("lifecycle.session_end", {
|
|
345
247
|
sessionId: input.session_id,
|
|
346
248
|
reason: input.reason,
|
|
347
249
|
});
|
|
348
|
-
client.
|
|
349
|
-
attributes: { reason: input.reason },
|
|
350
|
-
});
|
|
250
|
+
client.logger.activity("session.end", "ok", { attributes: { reason: input.reason } });
|
|
351
251
|
return {};
|
|
352
252
|
}
|
|
353
|
-
// ─── PreCompact ────────────────────────────────────────────────────
|
|
354
253
|
async function handlePreCompact(input, client) {
|
|
355
254
|
client.logger.info("lifecycle.pre_compact", {
|
|
356
255
|
sessionId: input.session_id,
|
|
357
256
|
trigger: input.trigger,
|
|
358
257
|
});
|
|
359
|
-
client.
|
|
258
|
+
client.logger.activity("compaction", "ok", {
|
|
360
259
|
attributes: { trigger: input.trigger, stage: "pre_compact" },
|
|
361
260
|
});
|
|
362
261
|
return {};
|
|
363
262
|
}
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
// Continue surfaces a structured permission decision via
|
|
367
|
-
// hookSpecificOutput.decision.{behavior}. Omitting `decision` falls through to
|
|
368
|
-
// Continue's normal user prompt. This event is trace-only on the auth side: it
|
|
369
|
-
// records the audit span and answers allow/deny based on policy, but never
|
|
370
|
-
// hard-blocks the session (the answer is advisory to the user prompt).
|
|
371
|
-
async function handlePermissionRequest(input, client) {
|
|
372
|
-
const toolName = input.tool_name ?? "unknown";
|
|
373
|
-
client.logger.info("lifecycle.permission_request", {
|
|
374
|
-
sessionId: input.session_id,
|
|
375
|
-
toolName,
|
|
376
|
-
toolInput: input.tool_input,
|
|
377
|
-
});
|
|
378
|
-
// Audit-only mode: omit `decision` so Continue falls through to its normal
|
|
379
|
-
// user-permission prompt.
|
|
380
|
-
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
381
|
-
return permissionRequestFallback("Audit-only mode — deferring to user");
|
|
382
|
-
}
|
|
383
|
-
// Interactive tools: the harness is about to surface UI to the user. Trace
|
|
384
|
-
// and fall through — Ory shouldn't pre-answer a prompt meant for the human.
|
|
385
|
-
if ((0, argus_1.isInteractiveTool)(HARNESS, toolName)) {
|
|
386
|
-
client.tracer.record("user.interaction", "ok", {
|
|
387
|
-
attributes: { harness: HARNESS, toolName, kind: "permission-request" },
|
|
388
|
-
});
|
|
389
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
390
|
-
}
|
|
391
|
-
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
392
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
393
|
-
const namespace = resolveNamespace();
|
|
394
|
-
try {
|
|
395
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
396
|
-
harness: HARNESS,
|
|
397
|
-
toolName,
|
|
398
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
399
|
-
spanAttributes: { toolName },
|
|
400
|
-
});
|
|
401
|
-
if (outcome.kind === "interactive") {
|
|
402
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
403
|
-
}
|
|
404
|
-
const decision = outcome;
|
|
405
|
-
if (decision.kind === "fail_open") {
|
|
406
|
-
const code = decision.error.code;
|
|
407
|
-
const fallbackReason = code === "network_error" || code === "rate_limited"
|
|
408
|
-
? `Ory unavailable (${code}), falling back to user prompt`
|
|
409
|
-
: `Ory permission check failed: ${decision.error.message}`;
|
|
410
|
-
return permissionRequestFallback(fallbackReason);
|
|
411
|
-
}
|
|
412
|
-
if (decision.kind === "deny") {
|
|
413
|
-
return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
|
|
414
|
-
}
|
|
415
|
-
if (decision.kind === "observe") {
|
|
416
|
-
// Observe-mode deny: the permission.observe_deny audit span is already
|
|
417
|
-
// recorded by the gate. Omit `decision` so Continue falls through to
|
|
418
|
-
// its native user prompt — observe mode must never escalate a
|
|
419
|
-
// policy-denied tool into an auto-approval.
|
|
420
|
-
return permissionRequestFallback("Ory observe mode — denied by policy, deferring to user");
|
|
421
|
-
}
|
|
422
|
-
return permissionRequestDecision("allow", "Ory permission granted");
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
const oryErr = err;
|
|
426
|
-
const fallbackReason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
|
|
427
|
-
? `Ory unavailable (${oryErr.code}), falling back to user prompt`
|
|
428
|
-
: `Ory permission check failed: ${oryErr.message}`;
|
|
429
|
-
return permissionRequestFallback(fallbackReason);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function permissionRequestDecision(behavior, reason) {
|
|
433
|
-
return {
|
|
434
|
-
hookSpecificOutput: {
|
|
435
|
-
hookEventName: "PermissionRequest",
|
|
436
|
-
decision: { behavior },
|
|
437
|
-
message: reason,
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function permissionRequestFallback(reason) {
|
|
442
|
-
return {
|
|
443
|
-
hookSpecificOutput: {
|
|
444
|
-
hookEventName: "PermissionRequest",
|
|
445
|
-
message: reason,
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
450
|
-
function resolveNamespace() {
|
|
451
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Register an OAuth2 identity for a sub-agent and write the
|
|
455
|
-
* `agent → subagent` delegation tuple. Fail-open and written at most once
|
|
456
|
-
* per install.
|
|
457
|
-
*/
|
|
458
|
-
async function registerSubAgent(subAgentType, client, deps) {
|
|
459
|
-
const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
|
|
460
|
-
let identity;
|
|
461
|
-
try {
|
|
462
|
-
identity = await subAgentGate(client, {
|
|
463
|
-
subAgentType,
|
|
464
|
-
projectUrl: (0, argus_1.resolveConfig)().projectUrl,
|
|
465
|
-
harness: HARNESS,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
catch (err) {
|
|
469
|
-
client.logger.warn("subagent.identity.failed", {
|
|
470
|
-
subAgentType,
|
|
471
|
-
message: err instanceof Error ? err.message : String(err),
|
|
472
|
-
});
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (identity.kind !== "dynamic" || !identity.subject)
|
|
476
|
-
return;
|
|
477
|
-
await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Fallback for the case where a sub-agent invocation appears as a Task/Agent
|
|
481
|
-
* tool call in PreToolUse with a `subagent_type` field on tool_input.
|
|
482
|
-
* registerSubAgent is idempotent so running both this and the dedicated
|
|
483
|
-
* SubagentStart path is harmless.
|
|
484
|
-
*/
|
|
485
|
-
async function maybeRegisterSubAgent(toolName, toolInput, client, deps) {
|
|
263
|
+
function subagentType(toolName, toolInput) {
|
|
486
264
|
if (toolName !== "Task" && toolName !== "Agent")
|
|
487
|
-
return;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
: undefined;
|
|
493
|
-
if (!subAgentType)
|
|
494
|
-
return;
|
|
495
|
-
await registerSubAgent(subAgentType, client, deps);
|
|
496
|
-
}
|
|
497
|
-
function handlePermissionError(oryErr, toolName, client) {
|
|
498
|
-
if (oryErr.code === "network_error") {
|
|
499
|
-
client.logger.warn("permission.network_error", {
|
|
500
|
-
toolName,
|
|
501
|
-
message: "Ory unreachable, failing open",
|
|
502
|
-
});
|
|
503
|
-
return {};
|
|
504
|
-
}
|
|
505
|
-
if (oryErr.code === "rate_limited") {
|
|
506
|
-
client.logger.warn("permission.rate_limited", {
|
|
507
|
-
toolName,
|
|
508
|
-
message: "Ory rate limited, failing open",
|
|
509
|
-
});
|
|
510
|
-
return {};
|
|
511
|
-
}
|
|
512
|
-
// For other errors, also fail open but log prominently
|
|
513
|
-
client.logger.error("permission.check.error", {
|
|
514
|
-
toolName,
|
|
515
|
-
code: oryErr.code,
|
|
516
|
-
message: oryErr.message,
|
|
517
|
-
});
|
|
518
|
-
return {};
|
|
265
|
+
return undefined;
|
|
266
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
267
|
+
return undefined;
|
|
268
|
+
const value = toolInput.subagent_type;
|
|
269
|
+
return typeof value === "string" ? value : undefined;
|
|
519
270
|
}
|
package/dist/hook.js
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const argus_1 = require("@ory/argus");
|
|
16
16
|
const handlers_js_1 = require("./handlers.js");
|
|
17
|
+
const EVENT_FLUSH_TIMEOUT_MS = 750;
|
|
18
|
+
let client;
|
|
19
|
+
async function flushEvents() {
|
|
20
|
+
try {
|
|
21
|
+
await client?.flushEvents(EVENT_FLUSH_TIMEOUT_MS);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// The hook decision must not depend on activity delivery.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
/**
|
|
18
28
|
* Read all of stdin as a string.
|
|
19
29
|
*
|
|
@@ -47,7 +57,7 @@ function readStdin() {
|
|
|
47
57
|
});
|
|
48
58
|
}
|
|
49
59
|
async function main() {
|
|
50
|
-
|
|
60
|
+
client = argus_1.OryAgentClient.fromEnv("continue");
|
|
51
61
|
const raw = await readStdin();
|
|
52
62
|
let input;
|
|
53
63
|
try {
|
|
@@ -55,22 +65,22 @@ async function main() {
|
|
|
55
65
|
}
|
|
56
66
|
catch {
|
|
57
67
|
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
58
|
-
await client.tracer.shutdown();
|
|
59
68
|
process.exit(0); // Don't block on parse errors
|
|
60
69
|
}
|
|
61
70
|
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
62
71
|
if (output.decision === "block") {
|
|
63
72
|
process.stdout.write(JSON.stringify(output));
|
|
64
|
-
await
|
|
73
|
+
await flushEvents();
|
|
65
74
|
process.exit(2);
|
|
66
75
|
}
|
|
67
76
|
if (output.hookSpecificOutput || output.systemMessage) {
|
|
68
77
|
process.stdout.write(JSON.stringify(output));
|
|
69
78
|
}
|
|
70
|
-
await
|
|
79
|
+
await flushEvents();
|
|
71
80
|
process.exit(0);
|
|
72
81
|
}
|
|
73
|
-
main().catch((err) => {
|
|
82
|
+
main().catch(async (err) => {
|
|
74
83
|
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
84
|
+
await flushEvents();
|
|
75
85
|
process.exit(0); // Fail open
|
|
76
86
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/continue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Ory plugin for Continue CLI (cn): 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,12 +66,11 @@
|
|
|
63
66
|
},
|
|
64
67
|
"files": [
|
|
65
68
|
"dist",
|
|
66
|
-
"!dist/dev",
|
|
67
69
|
"!dist/**/*.tsbuildinfo"
|
|
68
70
|
],
|
|
69
71
|
"dependencies": {
|
|
70
|
-
"
|
|
71
|
-
"@ory/argus": "0.
|
|
72
|
+
"reova": "^0.7.0",
|
|
73
|
+
"@ory/argus": "1.0.0"
|
|
72
74
|
},
|
|
73
75
|
"engines": {
|
|
74
76
|
"node": ">=22"
|
|
@@ -78,7 +80,6 @@
|
|
|
78
80
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
79
81
|
"test": "vitest run",
|
|
80
82
|
"test:watch": "vitest",
|
|
81
|
-
"typecheck": "tsc --noEmit"
|
|
82
|
-
"dev": "node dist/dev/launcher.js"
|
|
83
|
+
"typecheck": "tsc --noEmit"
|
|
83
84
|
}
|
|
84
85
|
}
|