@ory/amp 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 +73 -47
- package/dist/cli/main.js +22 -13
- package/dist/cli/setup.d.ts +4 -3
- package/dist/cli/setup.js +65 -37
- package/dist/permission.d.ts +16 -0
- package/dist/permission.js +62 -95
- package/dist/plugin.d.ts +1 -34
- package/dist/plugin.js +30 -149
- package/dist/types.d.ts +3 -3
- package/dist/types.js +2 -2
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Security and developer experience for [Amp](https://ampcode.com) (Sourcegraph's coding agent), powered by [Ory](https://ory.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Amp runs real actions on your machine — editing files, running shell commands, calling APIs. This plugin gives those actions an identity, a permission check, and an audit trail.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
One command installs two independent halves:
|
|
8
|
+
|
|
9
|
+
- **Developer experience** — the Ory skill catalog, the local Ory dev stack, and activity logging. No account, no keys, nothing to configure: it works the moment it's installed.
|
|
10
|
+
- **Ory Agent Security** — browser sign-in, brokered permission checks before tool calls, and the delegation trail. Opt-in connection details from the [Ory Console](https://console.ory.sh) switch it on, and it takes nothing away from the half above. See [Connect to Ory Agent Security](#connect-to-ory-agent-security).
|
|
8
11
|
|
|
9
12
|
## What you'll need
|
|
10
13
|
|
|
@@ -15,69 +18,90 @@ Security and developer experience for [Amp](https://ampcode.com) (Sourcegraph's
|
|
|
15
18
|
|
|
16
19
|
## Get started
|
|
17
20
|
|
|
18
|
-
Run one command
|
|
21
|
+
Run one command:
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
24
|
npx -y -p @ory/amp ory-amp install
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **Ory Network** *(default)* — sign in, or create a free account, in your browser. The project, keys, permissions, and login are all set up for you. Nothing to configure by hand.
|
|
27
|
-
- **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
|
|
28
|
-
- **Audit-only** — skip Ory entirely and just log what Amp does.
|
|
29
|
-
|
|
30
|
-
That's it. Confirm everything landed with:
|
|
27
|
+
Confirm everything landed with:
|
|
31
28
|
|
|
32
29
|
```bash
|
|
33
30
|
npx -y -p @ory/amp ory-amp status
|
|
34
31
|
```
|
|
35
32
|
|
|
36
|
-
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the plugin is wired into Amp, and recent activity.
|
|
33
|
+
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the plugin is wired into Amp, and recent activity. Until you connect Agent Security, the identity and permission rows say so and name what's missing.
|
|
34
|
+
|
|
35
|
+
## Skills and commands
|
|
36
|
+
|
|
37
|
+
Installing the plugin drops the full Ory playbook catalog into Amp. **Skills** are model-invoked — just say what you want in plain language and the matching one takes over.
|
|
38
|
+
|
|
39
|
+
| Skill | What it does for you |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
|
|
42
|
+
| `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
|
|
43
|
+
| `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
|
|
44
|
+
| `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
|
|
45
|
+
| `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
|
|
46
|
+
| `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
|
|
47
|
+
| `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
|
|
48
|
+
| `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
|
|
49
|
+
| `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
|
|
50
|
+
| `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
|
|
51
|
+
|
|
52
|
+
The local stack has its own playbooks — ask for them by name:
|
|
53
|
+
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `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 |
|
|
57
|
+
| `ory-local-down` | Stops it, keeping your data volumes |
|
|
58
|
+
| `ory-temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
|
|
37
59
|
|
|
38
|
-
|
|
60
|
+
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`.
|
|
61
|
+
|
|
62
|
+
So: ask Amp *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
|
|
39
63
|
|
|
40
64
|
## What you get
|
|
41
65
|
|
|
42
|
-
|
|
66
|
+
Out of the box, every tool Amp runs produces a privacy-safe structured activity event in the unified local log.
|
|
67
|
+
|
|
68
|
+
Once you connect to Ory Agent Security, two more things happen automatically:
|
|
43
69
|
|
|
44
|
-
- **Who's driving.** You sign in once in your browser; Amp gets its own identity
|
|
45
|
-
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **
|
|
46
|
-
- **A record of everything.** Every decision (allowed, denied, skipped) is logged as a trace you can send to a viewer like Jaeger or Honeycomb, or just a file.
|
|
70
|
+
- **Who's driving.** You sign in once in your browser; each Amp session gets its own identity and registers it automatically on first use. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later.
|
|
71
|
+
- **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.
|
|
47
72
|
|
|
48
73
|
If Ory is ever slow or unreachable, the plugin gets out of the way and lets Amp keep working — so it can't lock you out.
|
|
49
74
|
|
|
50
75
|
### See what's happening
|
|
51
76
|
|
|
52
|
-
Everything the plugin does is observable out of the box
|
|
77
|
+
Everything the plugin does is observable out of the box:
|
|
53
78
|
|
|
54
|
-
- **
|
|
55
|
-
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
79
|
+
- **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/amp/ory-agent-debug.log`. View events, decisions, and errors live with:
|
|
56
80
|
|
|
57
81
|
```bash
|
|
58
82
|
npx -y -p @ory/amp ory-amp watch
|
|
59
83
|
```
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
- **
|
|
85
|
+
Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
|
|
86
|
+
- **Live debug.** Launch Amp 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.
|
|
63
87
|
|
|
64
88
|
### Ready to enforce?
|
|
65
89
|
|
|
66
|
-
|
|
90
|
+
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.
|
|
67
91
|
|
|
68
92
|
```bash
|
|
69
|
-
npx -y -p @ory/amp ory-amp permissions
|
|
93
|
+
npx -y -p @ory/amp ory-amp permissions # what the project grants, and the live mode
|
|
70
94
|
```
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
Then a denied tool is actually blocked and Amp shows why.
|
|
73
97
|
|
|
74
98
|
<details>
|
|
75
99
|
<summary>How the plugin plugs into Amp</summary>
|
|
76
100
|
|
|
77
101
|
Amp gets the plugin in two pieces that work together:
|
|
78
102
|
|
|
79
|
-
- **A permission gate** registered in Amp's settings (`~/.config/amp/settings.json`) under the `amp.permissions` key. Before every tool call, Amp hands the tool and its params to this gate and reads the answer from its **exit code**: `0` = allow, `1` = ask, `≥2` = reject (the reason on stderr is shown to Amp).
|
|
80
|
-
- **An in-process plugin** at `<project>/.amp/plugins/ory.ts`, which Amp's built-in TypeScript runtime loads to sign you in
|
|
103
|
+
- **A permission gate** registered in Amp's settings (`~/.config/amp/settings.json`) under the `amp.permissions` key. Before every tool call, Amp hands the tool and its params to this compatibility gate and reads the answer from its **exit code**: `0` = allow, `1` = ask, `≥2` = reject (the reason on stderr is shown to Amp).
|
|
104
|
+
- **An in-process plugin** at `<project>/.amp/plugins/ory.ts`, which Amp's built-in TypeScript runtime loads to sign you in, authorize tool calls with Amp's exact thread ID, and record results. Its native `tool.call` response can block with `reject-and-continue`.
|
|
81
105
|
|
|
82
106
|
Amp gives the gate 10 seconds to answer and treats a slower gate as a reject, so the plugin sets its own shorter deadline (5 seconds, adjustable with `ORY_AMP_CHECK_TIMEOUT_MS`) and lets the tool through if the Ory check is slow, stuck, or unreachable — it never leaves you hanging.
|
|
83
107
|
|
|
@@ -85,38 +109,42 @@ The Ory skills land under `<project>/.agents/skills/`. `uninstall` removes the g
|
|
|
85
109
|
|
|
86
110
|
</details>
|
|
87
111
|
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
Beyond securing Amp, the plugin helps you build Ory into whatever you're working on. The Ory skills are **model-invoked** — just ask Amp in plain language and the right one takes over (Amp doesn't use slash commands for these). Ask Amp *"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 by asking *"start the local Ory stack"* (it prints a test email + password to sign in with).
|
|
91
|
-
|
|
92
|
-
More bundled **skills** cover login and social sign-in (Google, GitHub, Apple…), permissions onboarding, and playbooks for wiring Ory into your own agents, E2B sandboxes, or Temporal workers.
|
|
112
|
+
## Connect to Ory Agent Security
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
| Value | Flag | Environment variable |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| Project URL | `--project-url` | `ORY_PROJECT_URL` |
|
|
119
|
+
| Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
|
|
120
|
+
| Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
|
|
99
121
|
|
|
100
122
|
```bash
|
|
101
123
|
npx -y -p @ory/amp ory-amp configure \
|
|
102
124
|
--project-url https://<slug>.projects.oryapis.com \
|
|
103
|
-
--
|
|
104
|
-
--user-login
|
|
125
|
+
--agent-security-url https://agents.console.ory.com
|
|
105
126
|
```
|
|
106
127
|
|
|
107
|
-
|
|
128
|
+
`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`.
|
|
129
|
+
|
|
130
|
+
**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.
|
|
131
|
+
|
|
132
|
+
**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 Amp session registers its own identity automatically on first use.
|
|
133
|
+
|
|
134
|
+
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.
|
|
108
135
|
|
|
109
|
-
|
|
136
|
+
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.
|
|
110
137
|
|
|
111
138
|
## Commands
|
|
112
139
|
|
|
113
140
|
```
|
|
114
|
-
ory-amp install | uninstall Install
|
|
141
|
+
ory-amp install | uninstall Install (add --project-url to also connect Agent Security) / remove
|
|
115
142
|
ory-amp status Show configuration, identities, permission coverage, and recent activity
|
|
116
|
-
ory-amp
|
|
117
|
-
ory-amp
|
|
118
|
-
ory-amp configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --user-login, --audit-only)
|
|
143
|
+
ory-amp permissions <cmd> status (read-only; grants + posture live in the Ory Console)
|
|
144
|
+
ory-amp configure <flags> Connect a project (--project-url) or --disconnect
|
|
119
145
|
ory-amp agent <status|unregister> Manage Amp's own auto-created identity
|
|
146
|
+
ory-amp local <up|down|status|…> Run / manage a local Ory in Docker
|
|
147
|
+
ory-amp version Print plugin, core, and Node versions (--json for machine-readable)
|
|
120
148
|
```
|
|
121
149
|
|
|
122
150
|
All prefixed with `npx -y -p @ory/amp`.
|
|
@@ -124,16 +152,14 @@ All prefixed with `npx -y -p @ory/amp`.
|
|
|
124
152
|
## Troubleshooting
|
|
125
153
|
|
|
126
154
|
- **Local Ory fails to start** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
127
|
-
- **Browser sign-in loops** — reset with `ory-amp agent unregister` and try again.
|
|
128
|
-
- **
|
|
155
|
+
- **Browser sign-in loops** (after connecting) — reset with `ory-amp agent unregister` and try again.
|
|
156
|
+
- **Running an older CLI than expected** — `npx -p @ory/amp` (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/amp ory-amp version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/amp ory-amp install`. Pinning an exact version (`@ory/amp@<version>`) also bypasses the cached copy.
|
|
129
157
|
- **A tool check feels slow** — the plugin gives up on a stalled Ory check and lets the tool through; tune the deadline with `ORY_AMP_CHECK_TIMEOUT_MS`.
|
|
130
|
-
- **Want to see what's happening** — `npx -y -p @ory/amp ory-amp status` for a snapshot, `npx -y -p @ory/amp ory-amp 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/amp/` (see [See what's happening](#see-whats-happening)).
|
|
131
158
|
|
|
132
159
|
## Learn more
|
|
133
160
|
|
|
134
161
|
- [Ory documentation](https://www.ory.com/docs/) · [Ory Console](https://console.ory.sh) · [Ory Elements](https://github.com/ory/elements)
|
|
135
162
|
- [Amp documentation](https://ampcode.com)
|
|
136
|
-
- [Repo README](../../README.md) and [AGENTS.md](../../AGENTS.md) — full env-var and permission-mode reference
|
|
137
163
|
|
|
138
164
|
## License
|
|
139
165
|
|
package/dist/cli/main.js
CHANGED
|
@@ -51,6 +51,11 @@ const argus_1 = require("@ory/argus");
|
|
|
51
51
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
52
52
|
function main() {
|
|
53
53
|
const [command, ...args] = process.argv.slice(2);
|
|
54
|
+
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
55
|
+
// Without this the switch below ignores the flag and `install --help`
|
|
56
|
+
// performs a real install (#221).
|
|
57
|
+
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
58
|
+
return help();
|
|
54
59
|
switch (command) {
|
|
55
60
|
case "install":
|
|
56
61
|
(0, argus_1.beginDeferNextSteps)();
|
|
@@ -62,7 +67,10 @@ function main() {
|
|
|
62
67
|
break;
|
|
63
68
|
case "uninstall":
|
|
64
69
|
uninstall(args);
|
|
65
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
70
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
71
|
+
harness: "amp",
|
|
72
|
+
purge: args.includes("--purge"),
|
|
73
|
+
}).then(() => process.exit(0), (err) => {
|
|
66
74
|
console.error(err.message ?? err);
|
|
67
75
|
process.exit(1);
|
|
68
76
|
});
|
|
@@ -71,7 +79,7 @@ function main() {
|
|
|
71
79
|
(0, argus_1.runConfigureCommand)("ory-amp", args);
|
|
72
80
|
break;
|
|
73
81
|
case "agent":
|
|
74
|
-
(0, argus_1.runAgentCommand)("ory-amp", args).then((code) => process.exit(code), (err) => {
|
|
82
|
+
(0, argus_1.runAgentCommand)("ory-amp", "amp", args).then((code) => process.exit(code), (err) => {
|
|
75
83
|
console.error(err.message ?? err);
|
|
76
84
|
process.exit(1);
|
|
77
85
|
});
|
|
@@ -91,15 +99,15 @@ function main() {
|
|
|
91
99
|
process.exit(1);
|
|
92
100
|
});
|
|
93
101
|
break;
|
|
102
|
+
case "watch":
|
|
103
|
+
(0, argus_1.runWatchCommand)("ory-amp", "amp", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
104
|
+
break;
|
|
94
105
|
case "local":
|
|
95
106
|
(0, argus_1.runLocalCommand)("ory-amp", args).catch((err) => {
|
|
96
107
|
console.error(err.message ?? err);
|
|
97
108
|
process.exit(1);
|
|
98
109
|
});
|
|
99
110
|
break;
|
|
100
|
-
case "watch":
|
|
101
|
-
(0, argus_1.runWatchCommand)("amp", args);
|
|
102
|
-
break;
|
|
103
111
|
case "version":
|
|
104
112
|
case "--version":
|
|
105
113
|
case "-v":
|
|
@@ -156,13 +164,15 @@ async function status(args) {
|
|
|
156
164
|
const perms = Array.isArray(raw["amp.permissions"])
|
|
157
165
|
? raw["amp.permissions"]
|
|
158
166
|
: [];
|
|
159
|
-
delegateRegistered = perms.some((p) => p?.to === "ory-amp-permission"
|
|
167
|
+
delegateRegistered = perms.some((p) => p?.to === "ory-amp-permission" ||
|
|
168
|
+
/ory-hook-amp\.(js|cjs|mjs)$/.test(String(p?.to ?? "")));
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
catch {
|
|
163
172
|
/* ignore parse errors */
|
|
164
173
|
}
|
|
165
|
-
console.log(` Delegate: ${delegateRegistered ? "registered
|
|
174
|
+
console.log(` Delegate: ${delegateRegistered ? "registered" : "NOT registered"}`);
|
|
175
|
+
console.log(` Runtime: ${(0, argus_1.describeRuntimeHealth)((0, argus_1.checkRuntimeHealth)("amp"))}`);
|
|
166
176
|
console.log(` In-process plugin: ${fs.existsSync(pluginFile) ? `installed (${pluginFile})` : "not installed"}`);
|
|
167
177
|
},
|
|
168
178
|
});
|
|
@@ -179,23 +189,22 @@ Commands:
|
|
|
179
189
|
uninstall [--project-dir <path>] Remove the Ory delegate, plugin, and skills
|
|
180
190
|
configure Set or view Ory project URL and API key
|
|
181
191
|
agent <status|unregister> Manage the agent's OAuth2 (DCR) identity
|
|
182
|
-
permissions
|
|
183
|
-
(status, bootstrap, observe, enforce)
|
|
192
|
+
permissions Show the live permission mode and per-tool coverage
|
|
184
193
|
setup [--project-dir <path>] Write config directly (fallback)
|
|
185
194
|
status [--project-dir <path>] Show plugin configuration and status
|
|
186
|
-
watch [
|
|
195
|
+
watch [--json] [--lines <count>] Follow the live activity/debug log
|
|
187
196
|
version Show version and the ory-agent-plugins build commit
|
|
188
197
|
local <cmd> Manage local Ory dev environment
|
|
189
198
|
(up, down, status, seed, logs, env, configure, reset)
|
|
190
199
|
|
|
191
200
|
After installing, the Ory integration runs in two parts:
|
|
192
201
|
Permission delegate (ory-amp-permission) Authorize each tool call (blocking)
|
|
193
|
-
In-process plugin (.amp/plugins/ory.ts) Session auth +
|
|
202
|
+
In-process plugin (.amp/plugins/ory.ts) Session auth + activity auditing
|
|
194
203
|
|
|
195
204
|
Examples:
|
|
196
205
|
npx -y -p @ory/amp ory-amp install
|
|
197
|
-
npx -y -p @ory/amp ory-amp configure --project-url https://<slug>.projects.oryapis.com
|
|
198
|
-
npx -y -p @ory/amp ory-amp permissions
|
|
206
|
+
npx -y -p @ory/amp ory-amp configure --project-url https://<slug>.projects.oryapis.com --agent-security-url https://agents.console.ory.com
|
|
207
|
+
npx -y -p @ory/amp ory-amp permissions
|
|
199
208
|
npx -y -p @ory/amp ory-amp status
|
|
200
209
|
`);
|
|
201
210
|
}
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Performs two installs:
|
|
6
6
|
* (a) Registers the permission delegate in Amp's settings under
|
|
7
|
-
* `amp.permissions` as `{ tool: "*", action: "delegate",
|
|
8
|
-
*
|
|
7
|
+
* `amp.permissions` as `{ tool: "*", action: "delegate", to: <path> }`,
|
|
8
|
+
* where the path is the Ory runtime shim resolved at install time — this
|
|
9
|
+
* is the blocking gate.
|
|
9
10
|
* (b) Writes the in-process plugin loader at
|
|
10
11
|
* `<projectDir>/.amp/plugins/ory.ts` so Amp's Bun TypeScript runtime
|
|
11
|
-
* discovers it for session auth +
|
|
12
|
+
* discovers it for session auth + activity auditing.
|
|
12
13
|
*
|
|
13
14
|
* Usage:
|
|
14
15
|
* npx ory-amp-setup # Auto-detect and configure
|
package/dist/cli/setup.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Performs two installs:
|
|
7
7
|
* (a) Registers the permission delegate in Amp's settings under
|
|
8
|
-
* `amp.permissions` as `{ tool: "*", action: "delegate",
|
|
9
|
-
*
|
|
8
|
+
* `amp.permissions` as `{ tool: "*", action: "delegate", to: <path> }`,
|
|
9
|
+
* where the path is the Ory runtime shim resolved at install time — this
|
|
10
|
+
* is the blocking gate.
|
|
10
11
|
* (b) Writes the in-process plugin loader at
|
|
11
12
|
* `<projectDir>/.amp/plugins/ory.ts` so Amp's Bun TypeScript runtime
|
|
12
|
-
* discovers it for session auth +
|
|
13
|
+
* discovers it for session auth + activity auditing.
|
|
13
14
|
*
|
|
14
15
|
* Usage:
|
|
15
16
|
* npx ory-amp-setup # Auto-detect and configure
|
|
@@ -70,22 +71,26 @@ const path = __importStar(require("node:path"));
|
|
|
70
71
|
const argus_1 = require("@ory/argus");
|
|
71
72
|
const assets_js_1 = require("./assets.js");
|
|
72
73
|
const PACKAGE_NAME = "@ory/amp";
|
|
73
|
-
const
|
|
74
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
74
75
|
/** Direct *.ts loader Amp's Bun runtime discovers under .amp/plugins/. */
|
|
75
76
|
const PLUGIN_FILE = "ory.ts";
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* Resolve this install's runtime and write its shims.
|
|
79
|
+
*
|
|
80
|
+
* Amp needs both halves: the permission delegate is a *program* Amp executes
|
|
81
|
+
* (so it gets the hook shim), while activity auditing is an in-process
|
|
82
|
+
* plugin Amp's Bun runtime imports (so the loader re-exports the runtime's main
|
|
83
|
+
* module by absolute path).
|
|
80
84
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
function wireAmpRuntime(args) {
|
|
86
|
+
return (0, argus_1.wireRuntime)({
|
|
87
|
+
harness: "amp",
|
|
88
|
+
packageName: PACKAGE_NAME,
|
|
89
|
+
packageRoot: PACKAGE_ROOT,
|
|
90
|
+
installCommand: "npx -y -p @ory/amp ory-amp install",
|
|
91
|
+
args,
|
|
92
|
+
entryRelPath: path.join("dist", "permission.js"),
|
|
93
|
+
});
|
|
89
94
|
}
|
|
90
95
|
function getSettingsPath(_args) {
|
|
91
96
|
// Verified: Amp's default settings file is the user-global
|
|
@@ -100,23 +105,31 @@ function getSettingsPath(_args) {
|
|
|
100
105
|
* params on stdin and reads the decision from the exit code.
|
|
101
106
|
*
|
|
102
107
|
* Verified against the binary: a flat top-level `amp.permissions` array of
|
|
103
|
-
* `{ tool, action, to }` entries
|
|
108
|
+
* `{ tool, action, to }` entries, and `to` accepts an **absolute path** — its
|
|
109
|
+
* help text says the helper "must be on $PATH", but a path works and is what
|
|
110
|
+
* we write. That matters beyond consistency: a bare bin name only resolves
|
|
111
|
+
* from a shell-initialized PATH, which a GUI-launched Amp does not have.
|
|
104
112
|
*/
|
|
105
|
-
function delegateEntry() {
|
|
106
|
-
return { tool: "*", action: "delegate", to
|
|
113
|
+
function delegateEntry(to) {
|
|
114
|
+
return { tool: "*", action: "delegate", to };
|
|
107
115
|
}
|
|
108
116
|
function isOryDelegate(entry) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
if (typeof entry !== "object" || entry === null)
|
|
118
|
+
return false;
|
|
119
|
+
const rec = entry;
|
|
120
|
+
if (rec.action !== "delegate")
|
|
121
|
+
return false;
|
|
122
|
+
const to = typeof rec.to === "string" ? rec.to : "";
|
|
123
|
+
// Match the shim we write now and the bare bin name earlier releases wrote,
|
|
124
|
+
// so an install replaces a stale entry instead of leaving both in place.
|
|
125
|
+
return to === "ory-amp-permission" || /ory-hook-amp\.(js|cjs|mjs)$/.test(to);
|
|
113
126
|
}
|
|
114
|
-
function mergePermissions(existing) {
|
|
127
|
+
function mergePermissions(existing, to) {
|
|
115
128
|
const merged = { ...existing };
|
|
116
129
|
const perms = (Array.isArray(merged["amp.permissions"])
|
|
117
130
|
? merged["amp.permissions"]
|
|
118
131
|
: []).filter((e) => !isOryDelegate(e));
|
|
119
|
-
perms.push(delegateEntry());
|
|
132
|
+
perms.push(delegateEntry(to));
|
|
120
133
|
merged["amp.permissions"] = perms;
|
|
121
134
|
return merged;
|
|
122
135
|
}
|
|
@@ -136,14 +149,15 @@ function removePermissions(existing) {
|
|
|
136
149
|
/**
|
|
137
150
|
* The in-process plugin loader written at `.amp/plugins/ory.ts`. Amp's Bun
|
|
138
151
|
* TypeScript runtime discovers direct `*.ts` files under `.amp/plugins/`, so
|
|
139
|
-
* this re-exports the
|
|
140
|
-
*
|
|
141
|
-
*
|
|
152
|
+
* this re-exports the resolved runtime's default-exported plugin factory by
|
|
153
|
+
* absolute path — a bare module id would depend on `@ory/amp` being resolvable
|
|
154
|
+
* from wherever Amp runs.
|
|
142
155
|
*/
|
|
143
|
-
function pluginLoaderSource(
|
|
144
|
-
|
|
156
|
+
function pluginLoaderSource(runtime) {
|
|
157
|
+
const entry = path.join(runtime.target.packageDir, "dist", "index.js");
|
|
158
|
+
return (`// Generated by ${PACKAGE_NAME}@${runtime.target.version} — do not edit by hand.\n` +
|
|
145
159
|
`// Re-exports the Ory in-process plugin for Amp's .amp/plugins/ loader.\n` +
|
|
146
|
-
`export { default } from
|
|
160
|
+
`export { default } from ${JSON.stringify(entry)};\n`);
|
|
147
161
|
}
|
|
148
162
|
function getPluginFile(args) {
|
|
149
163
|
return path.join(args.projectDir, ".amp", "plugins", PLUGIN_FILE);
|
|
@@ -154,9 +168,9 @@ function main() {
|
|
|
154
168
|
process.exit(0);
|
|
155
169
|
}
|
|
156
170
|
const args = (0, argus_1.parseSetupArgs)();
|
|
157
|
-
const version = readPackageVersion();
|
|
158
171
|
if (args.print) {
|
|
159
|
-
|
|
172
|
+
// Nothing is resolved or written on a --print run.
|
|
173
|
+
console.log(JSON.stringify({ "amp.permissions": [delegateEntry("<resolved runtime shim>")] }, null, 2));
|
|
160
174
|
return;
|
|
161
175
|
}
|
|
162
176
|
const settingsPath = getSettingsPath(args);
|
|
@@ -178,18 +192,29 @@ function main() {
|
|
|
178
192
|
}
|
|
179
193
|
(0, assets_js_1.uninstallAmpOryAssets)(args.projectDir);
|
|
180
194
|
console.log(`Removed Ory skills from ${skillsDir}`);
|
|
195
|
+
(0, argus_1.removeRuntimeWiring)("amp");
|
|
196
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
197
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
198
|
+
}
|
|
181
199
|
return;
|
|
182
200
|
}
|
|
201
|
+
const runtime = wireAmpRuntime(process.argv.slice(2));
|
|
202
|
+
console.log(runtime.target.kind === "linked"
|
|
203
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
204
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
205
|
+
for (const pruned of runtime.prunedStores) {
|
|
206
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
207
|
+
}
|
|
183
208
|
// (a) Register the permission delegate (blocking gate).
|
|
184
|
-
const merged = mergePermissions(existing);
|
|
209
|
+
const merged = mergePermissions(existing, runtime.hookShim);
|
|
185
210
|
(0, argus_1.writeJsonFile)(settingsPath, merged);
|
|
186
|
-
// (b) Write the in-process plugin loader (auth +
|
|
211
|
+
// (b) Write the in-process plugin loader (auth + activity auditing).
|
|
187
212
|
fs.mkdirSync(path.dirname(pluginFile), { recursive: true });
|
|
188
|
-
fs.writeFileSync(pluginFile, pluginLoaderSource(
|
|
213
|
+
fs.writeFileSync(pluginFile, pluginLoaderSource(runtime));
|
|
189
214
|
// (c) Materialize the Ory skill catalog.
|
|
190
215
|
(0, assets_js_1.installAmpOryAssets)(args.projectDir);
|
|
191
216
|
console.log(`Ory permission delegate registered in ${settingsPath}`);
|
|
192
|
-
console.log(` Delegate: ${
|
|
217
|
+
console.log(` Delegate: ${runtime.hookShim} (tool: "*", action: delegate)`);
|
|
193
218
|
console.log(`Ory in-process plugin written to ${pluginFile}`);
|
|
194
219
|
console.log(`Ory skills installed under ${skillsDir}`);
|
|
195
220
|
(0, argus_1.printNextSteps)("Amp", "npx -y -p @ory/amp ory-amp-setup --uninstall", {
|
|
@@ -202,7 +227,10 @@ main();
|
|
|
202
227
|
// stored Ory credentials. When required by the plugin's main CLI, that
|
|
203
228
|
// command owns the purge, so the `require.main` guard prevents a double run.
|
|
204
229
|
if (require.main === module && process.argv.includes("--uninstall")) {
|
|
205
|
-
(0, argus_1.clearCredentialsForUninstall)(
|
|
230
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
231
|
+
harness: "amp",
|
|
232
|
+
purge: process.argv.includes("--purge"),
|
|
233
|
+
}).then(() => process.exit(0), (err) => {
|
|
206
234
|
console.error(err.message ?? err);
|
|
207
235
|
process.exit(1);
|
|
208
236
|
});
|
package/dist/permission.d.ts
CHANGED
|
@@ -47,3 +47,19 @@ export declare function extractToolName(input: AmpDelegateInput): string;
|
|
|
47
47
|
* error resolves to ALLOW.
|
|
48
48
|
*/
|
|
49
49
|
export declare function decideToolPermission(input: AmpDelegateInput, client: OryAgentClient): Promise<DelegateDecision>;
|
|
50
|
+
/**
|
|
51
|
+
* The stdin→exit-code delegate pipeline.
|
|
52
|
+
*
|
|
53
|
+
* **Exported on purpose.** This module must stay side-effect-free on import —
|
|
54
|
+
* tests call `decideToolPermission` directly, and `main()` ends in
|
|
55
|
+
* `process.exit` — but it also has to run when the generated hook shim loads it
|
|
56
|
+
* with `require()`. Under `require()` the old `require.main === module` guard was
|
|
57
|
+
* false (`require.main` is the *shim*), so `main()` never ran, the process exited
|
|
58
|
+
* 0, and Amp reads exit 0 as **allow** — every tool call passed unchecked, with
|
|
59
|
+
* no activity event and no log (#217).
|
|
60
|
+
*
|
|
61
|
+
* So the entry point is explicit in both directions: the guard below still runs
|
|
62
|
+
* it when this file *is* the process entry, and the shim calls this export when
|
|
63
|
+
* it is not. Neither fires on a bare import.
|
|
64
|
+
*/
|
|
65
|
+
export declare function main(): Promise<void>;
|
package/dist/permission.js
CHANGED
|
@@ -33,11 +33,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
33
33
|
exports.resolveCheckTimeoutMs = resolveCheckTimeoutMs;
|
|
34
34
|
exports.extractToolName = extractToolName;
|
|
35
35
|
exports.decideToolPermission = decideToolPermission;
|
|
36
|
+
exports.main = main;
|
|
36
37
|
const argus_1 = require("@ory/argus");
|
|
37
38
|
const types_js_1 = require("./types.js");
|
|
38
|
-
function resolveNamespace() {
|
|
39
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
40
|
-
}
|
|
41
39
|
/**
|
|
42
40
|
* Local deadline for the Ory permission check. Amp treats a delegate that
|
|
43
41
|
* does not exit within 10s as a REJECT, so a hanging backend must be cut
|
|
@@ -45,6 +43,16 @@ function resolveNamespace() {
|
|
|
45
43
|
* default leaves ample headroom inside Amp's 10s window.
|
|
46
44
|
*/
|
|
47
45
|
const DEFAULT_CHECK_TIMEOUT_MS = 5000;
|
|
46
|
+
const EVENT_FLUSH_TIMEOUT_MS = 750;
|
|
47
|
+
let entrypointClient;
|
|
48
|
+
async function flushEvents() {
|
|
49
|
+
try {
|
|
50
|
+
await entrypointClient?.flushEvents(EVENT_FLUSH_TIMEOUT_MS);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// The delegate decision must not depend on activity delivery.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
/** Resolve the check deadline, overridable via ORY_AMP_CHECK_TIMEOUT_MS. */
|
|
49
57
|
function resolveCheckTimeoutMs() {
|
|
50
58
|
const raw = process.env.ORY_AMP_CHECK_TIMEOUT_MS;
|
|
@@ -93,96 +101,39 @@ function extractSessionId(input) {
|
|
|
93
101
|
async function decideToolPermission(input, client) {
|
|
94
102
|
const toolName = extractToolName(input);
|
|
95
103
|
const sessionId = extractSessionId(input);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
attributes: { toolName, mode: "audit-only" },
|
|
105
|
-
});
|
|
106
|
-
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
107
|
-
}
|
|
108
|
-
const subject = (0, argus_1.resolveUserSubject)(client, sessionId ? `session:${sessionId}` : undefined);
|
|
109
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
110
|
-
try {
|
|
111
|
-
// Race against a local deadline: core's checkPermission has no HTTP
|
|
112
|
-
// timeout, and Amp treats a delegate that outlives its 10s window as
|
|
113
|
-
// a reject — a hanging backend must fail OPEN, not closed.
|
|
114
|
-
const timeoutMs = resolveCheckTimeoutMs();
|
|
115
|
-
const raced = await raceCheckDeadline((0, argus_1.gateToolCall)(client, {
|
|
116
|
-
harness: "amp",
|
|
117
|
-
toolName,
|
|
118
|
-
check: {
|
|
119
|
-
namespace: resolveNamespace(),
|
|
120
|
-
object: toolName,
|
|
121
|
-
relation: "use",
|
|
122
|
-
...subject,
|
|
123
|
-
},
|
|
124
|
-
spanAttributes: { toolName },
|
|
125
|
-
}), timeoutMs);
|
|
126
|
-
if (raced === CHECK_TIMED_OUT) {
|
|
127
|
-
process.stderr.write(`[ory-agent] permission check for "${toolName}" timed out after ${timeoutMs}ms; allowing (fail-open)\n`);
|
|
128
|
-
client.logger.warn("delegate.check_timeout", { toolName, timeoutMs });
|
|
129
|
-
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
130
|
-
}
|
|
131
|
-
const outcome = raced;
|
|
132
|
-
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
133
|
-
// the user.interaction span is already recorded; allow so Amp can
|
|
134
|
-
// surface the prompt to the user.
|
|
135
|
-
if (outcome.kind === "interactive") {
|
|
136
|
-
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
137
|
-
}
|
|
138
|
-
const decision = outcome;
|
|
139
|
-
const attrs = { toolName };
|
|
140
|
-
const decisionAttrs = decision.spanAttributes;
|
|
141
|
-
if (decision.kind === "fail_open") {
|
|
142
|
-
// network_error / rate_limited / unknown → allow.
|
|
143
|
-
client.logger.warn("delegate.fail_open", {
|
|
104
|
+
return (0, argus_1.withHookContext)(client, { sessionId }, async () => {
|
|
105
|
+
client.logger.info("delegate.received", { toolName, sessionId });
|
|
106
|
+
try {
|
|
107
|
+
// Amp rejects delegates that exceed ten seconds, so retain a shorter
|
|
108
|
+
// local deadline that turns a hanging backend into an allow.
|
|
109
|
+
const timeoutMs = resolveCheckTimeoutMs();
|
|
110
|
+
const raced = await raceCheckDeadline((0, argus_1.gate)(client, {
|
|
111
|
+
harness: "amp",
|
|
144
112
|
toolName,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
return
|
|
113
|
+
toolArgs: input.input ?? input.params ?? input.arguments ?? input.args,
|
|
114
|
+
subjectFallback: sessionId ? `session:${sessionId}` : undefined,
|
|
115
|
+
canBlock: true,
|
|
116
|
+
}), timeoutMs);
|
|
117
|
+
if (raced === CHECK_TIMED_OUT) {
|
|
118
|
+
process.stderr.write(`[ory-agent] permission check for "${toolName}" timed out after ${timeoutMs}ms; allowing (fail-open)\n`);
|
|
119
|
+
client.logger.warn("delegate.check_timeout", { toolName, timeoutMs });
|
|
120
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
121
|
+
}
|
|
122
|
+
return raced.blocked
|
|
123
|
+
? {
|
|
124
|
+
exitCode: types_js_1.AMP_DELEGATE_EXIT.REJECT,
|
|
125
|
+
reason: raced.denialMessage ?? "Tool denied by Ory",
|
|
126
|
+
}
|
|
127
|
+
: { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
155
128
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
162
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
129
|
+
catch (err) {
|
|
130
|
+
client.logger.error("delegate.error", {
|
|
131
|
+
toolName,
|
|
132
|
+
message: err instanceof Error ? err.message : String(err),
|
|
163
133
|
});
|
|
164
134
|
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
165
135
|
}
|
|
166
|
-
|
|
167
|
-
client.tracer.record("tool.block", "denied", {
|
|
168
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
169
|
-
});
|
|
170
|
-
const reason = (0, argus_1.formatDenialMessage)({
|
|
171
|
-
tool: toolName,
|
|
172
|
-
subjectId,
|
|
173
|
-
namespace: resolveNamespace(),
|
|
174
|
-
});
|
|
175
|
-
client.logger.warn("tool.denied", { toolName, subjectId, message: reason });
|
|
176
|
-
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.REJECT, reason };
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
// Fail open on any unexpected error.
|
|
180
|
-
client.logger.error("delegate.error", {
|
|
181
|
-
toolName,
|
|
182
|
-
message: err instanceof Error ? err.message : String(err),
|
|
183
|
-
});
|
|
184
|
-
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
185
|
-
}
|
|
136
|
+
});
|
|
186
137
|
}
|
|
187
138
|
/**
|
|
188
139
|
* Read all of stdin as a string.
|
|
@@ -217,8 +168,24 @@ function readStdin() {
|
|
|
217
168
|
});
|
|
218
169
|
});
|
|
219
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The stdin→exit-code delegate pipeline.
|
|
173
|
+
*
|
|
174
|
+
* **Exported on purpose.** This module must stay side-effect-free on import —
|
|
175
|
+
* tests call `decideToolPermission` directly, and `main()` ends in
|
|
176
|
+
* `process.exit` — but it also has to run when the generated hook shim loads it
|
|
177
|
+
* with `require()`. Under `require()` the old `require.main === module` guard was
|
|
178
|
+
* false (`require.main` is the *shim*), so `main()` never ran, the process exited
|
|
179
|
+
* 0, and Amp reads exit 0 as **allow** — every tool call passed unchecked, with
|
|
180
|
+
* no activity event and no log (#217).
|
|
181
|
+
*
|
|
182
|
+
* So the entry point is explicit in both directions: the guard below still runs
|
|
183
|
+
* it when this file *is* the process entry, and the shim calls this export when
|
|
184
|
+
* it is not. Neither fires on a bare import.
|
|
185
|
+
*/
|
|
220
186
|
async function main() {
|
|
221
187
|
const client = argus_1.OryAgentClient.fromEnv("amp");
|
|
188
|
+
entrypointClient = client;
|
|
222
189
|
const raw = await readStdin();
|
|
223
190
|
let input;
|
|
224
191
|
try {
|
|
@@ -227,7 +194,6 @@ async function main() {
|
|
|
227
194
|
catch {
|
|
228
195
|
// Parse failure — fail open (allow) rather than wedging the agent.
|
|
229
196
|
client.logger.error("delegate.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
230
|
-
await client.tracer.shutdown();
|
|
231
197
|
process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
|
|
232
198
|
}
|
|
233
199
|
const { exitCode, reason } = await decideToolPermission(input, client);
|
|
@@ -235,17 +201,18 @@ async function main() {
|
|
|
235
201
|
// stderr is forwarded to the model as the rejection reason.
|
|
236
202
|
process.stderr.write(reason + "\n");
|
|
237
203
|
}
|
|
238
|
-
await
|
|
204
|
+
await flushEvents();
|
|
239
205
|
process.exit(exitCode);
|
|
240
206
|
}
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
207
|
+
// Run when this file IS the process entry (`node …/permission.js`). When the
|
|
208
|
+
// generated shim `require()`s it instead, `require.main` is the shim, so this is
|
|
209
|
+
// false and the shim calls the exported `main` above. Tests import the module and
|
|
210
|
+
// trigger neither. `module` exists under CJS output (module: nodenext → CJS).
|
|
245
211
|
if (require.main === module) {
|
|
246
|
-
main().catch((err) => {
|
|
212
|
+
main().catch(async (err) => {
|
|
247
213
|
// Top-level safety net: never block on an unexpected crash.
|
|
248
214
|
process.stderr.write(`[ory-agent] delegate fatal: ${err}\n`);
|
|
215
|
+
await flushEvents();
|
|
249
216
|
process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
|
|
250
217
|
});
|
|
251
218
|
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
|
|
3
|
-
*
|
|
4
|
-
* Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
|
|
5
|
-
* calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
|
|
6
|
-
* Each handler receives the event payload and a `PluginEventContext`. We
|
|
7
|
-
* subscribe to:
|
|
8
|
-
*
|
|
9
|
-
* session.start → run the user gate (advisory, allowBlock:false), the
|
|
10
|
-
* agent gate, and write the user→agent delegation tuple.
|
|
11
|
-
* tool.result → record a `tool.complete` audit span.
|
|
12
|
-
*
|
|
13
|
-
* The BLOCKING permission decision deliberately does NOT live here. Amp's
|
|
14
|
-
* in-process `tool.call` event *can* block (via a `reject-and-continue`
|
|
15
|
-
* result), but we route blocking through the standalone permission delegate
|
|
16
|
-
* helper (`permission.ts`) instead — that gives the same subprocess parity
|
|
17
|
-
* as the other harnesses and keeps the in-process module purely
|
|
18
|
-
* observational. The delegate runs as its own process and decides by exit
|
|
19
|
-
* code; this module only authenticates and traces.
|
|
20
|
-
*
|
|
21
|
-
* Because Amp's `session.start` handler has no return-value channel for
|
|
22
|
-
* blocking, the user login runs in advisory mode (allowBlock:false), like
|
|
23
|
-
* the OpenCode / OpenClaw factory plugins.
|
|
24
|
-
*/
|
|
25
|
-
import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
|
|
1
|
+
import { OryAgentClient, ensureAgentIdentity, ensureUserAuthenticated } from "@ory/argus";
|
|
26
2
|
import type { AmpPlugin } from "./types.js";
|
|
27
3
|
export interface CreateOryPluginDeps {
|
|
28
|
-
/** Test injection point for the user login flow. */
|
|
29
4
|
userLogin?: typeof ensureUserAuthenticated;
|
|
30
|
-
/** Test injection point for the agent identity gate. */
|
|
31
5
|
agentGate?: typeof ensureAgentIdentity;
|
|
32
6
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Create the Amp in-process plugin factory.
|
|
35
|
-
*
|
|
36
|
-
* Returns a function matching Amp's `AmpPlugin` contract: it receives the
|
|
37
|
-
* PluginAPI and registers event handlers via `api.on(...)`.
|
|
38
|
-
*/
|
|
39
7
|
export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
|
|
40
8
|
projectUrl: string;
|
|
41
|
-
apiKey?: string;
|
|
42
9
|
}, deps?: CreateOryPluginDeps): AmpPlugin;
|
package/dist/plugin.js
CHANGED
|
@@ -1,181 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
|
|
4
|
-
*
|
|
5
|
-
* Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
|
|
6
|
-
* calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
|
|
7
|
-
* Each handler receives the event payload and a `PluginEventContext`. We
|
|
8
|
-
* subscribe to:
|
|
9
|
-
*
|
|
10
|
-
* session.start → run the user gate (advisory, allowBlock:false), the
|
|
11
|
-
* agent gate, and write the user→agent delegation tuple.
|
|
12
|
-
* tool.result → record a `tool.complete` audit span.
|
|
13
|
-
*
|
|
14
|
-
* The BLOCKING permission decision deliberately does NOT live here. Amp's
|
|
15
|
-
* in-process `tool.call` event *can* block (via a `reject-and-continue`
|
|
16
|
-
* result), but we route blocking through the standalone permission delegate
|
|
17
|
-
* helper (`permission.ts`) instead — that gives the same subprocess parity
|
|
18
|
-
* as the other harnesses and keeps the in-process module purely
|
|
19
|
-
* observational. The delegate runs as its own process and decides by exit
|
|
20
|
-
* code; this module only authenticates and traces.
|
|
21
|
-
*
|
|
22
|
-
* Because Amp's `session.start` handler has no return-value channel for
|
|
23
|
-
* blocking, the user login runs in advisory mode (allowBlock:false), like
|
|
24
|
-
* the OpenCode / OpenClaw factory plugins.
|
|
25
|
-
*/
|
|
26
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
3
|
exports.createOryPlugin = createOryPlugin;
|
|
28
4
|
const argus_1 = require("@ory/argus");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
* Returns a function matching Amp's `AmpPlugin` contract: it receives the
|
|
33
|
-
* PluginAPI and registers event handlers via `api.on(...)`.
|
|
34
|
-
*/
|
|
5
|
+
const permission_js_1 = require("./permission.js");
|
|
6
|
+
const HARNESS = "amp";
|
|
35
7
|
function createOryPlugin(clientOrConfig, deps = {}) {
|
|
36
8
|
return (api) => {
|
|
37
9
|
const client = clientOrConfig instanceof argus_1.OryAgentClient
|
|
38
10
|
? clientOrConfig
|
|
39
11
|
: clientOrConfig
|
|
40
|
-
? new argus_1.OryAgentClient({ ...clientOrConfig, harness:
|
|
41
|
-
: argus_1.OryAgentClient.fromEnv(
|
|
12
|
+
? new argus_1.OryAgentClient({ ...clientOrConfig, harness: HARNESS })
|
|
13
|
+
: argus_1.OryAgentClient.fromEnv(HARNESS);
|
|
42
14
|
api.on("session.start", createSessionStartHandler(client, deps));
|
|
15
|
+
api.on("tool.call", createToolCallHandler(client));
|
|
43
16
|
api.on("tool.result", createToolResultHandler(client));
|
|
44
17
|
};
|
|
45
18
|
}
|
|
46
|
-
// ─── session.start ─────────────────────────────────────────────────
|
|
47
19
|
function createSessionStartHandler(client, deps) {
|
|
48
|
-
return
|
|
20
|
+
return (event, _ctx) => (0, argus_1.withHookContext)(client, { sessionId: event.thread?.id }, async () => {
|
|
49
21
|
const sessionId = event.thread?.id;
|
|
50
22
|
client.logger.info("lifecycle.session_start", { sessionId });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
sessionId,
|
|
54
|
-
});
|
|
55
|
-
client.tracer.record("session.start", "ok", {
|
|
56
|
-
attributes: sessionId ? { sessionId } : {},
|
|
57
|
-
});
|
|
58
|
-
// Run the user login. Amp's session.start has no block primitive, so
|
|
59
|
-
// allowBlock is false — the flow emits the user.auth audit span,
|
|
60
|
-
// refreshes tokens, and may prompt when interactive, but never
|
|
61
|
-
// prevents the session from starting. When ORY_USER_LOGIN is unset it
|
|
62
|
-
// is a no-op (mode === "disabled") and we fall through to legacy
|
|
63
|
-
// logging.
|
|
64
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
65
|
-
const decision = await userGate(client, {
|
|
23
|
+
await (0, argus_1.sessionStart)(client, {
|
|
24
|
+
harness: HARNESS,
|
|
66
25
|
binName: "ory-amp",
|
|
67
|
-
|
|
68
|
-
|
|
26
|
+
userLogin: deps.userLogin,
|
|
27
|
+
agentGate: deps.agentGate,
|
|
69
28
|
});
|
|
70
|
-
|
|
71
|
-
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
72
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
73
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "amp" });
|
|
74
|
-
// Once both principals are populated, write the user→agent delegation
|
|
75
|
-
// tuple. Written at most once per install + fail-open: audit-trail data
|
|
76
|
-
// only.
|
|
77
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
78
|
-
if (decision.mode !== "disabled") {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const resolved = (0, argus_1.resolveConfig)();
|
|
82
|
-
if (resolved.auditOnly) {
|
|
83
|
-
client.logger.info("config.audit_only", {
|
|
84
|
-
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
85
|
-
});
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (!resolved.projectUrl) {
|
|
89
|
-
client.logger.warn("config.not_configured", {
|
|
90
|
-
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
91
|
-
"Run 'npx ory-amp configure' to connect to an Ory project.",
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
96
|
-
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
97
|
-
if (sessionToken) {
|
|
98
|
-
await verifySessionToken(client, sessionToken);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (oauth2Token) {
|
|
102
|
-
await verifyOAuth2Token(client, oauth2Token);
|
|
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
|
-
};
|
|
29
|
+
});
|
|
110
30
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
client.logger.warn("session.verify_failed", {
|
|
122
|
-
code: isOryError(err) ? err.code : "unknown",
|
|
123
|
-
message: err instanceof Error ? err.message : String(err),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
async function verifyOAuth2Token(client, token) {
|
|
128
|
-
try {
|
|
129
|
-
const tokenInfo = await client.introspectToken(token);
|
|
130
|
-
if (!tokenInfo.active) {
|
|
131
|
-
client.logger.warn("oauth2.token_inactive", {
|
|
132
|
-
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
137
|
-
clientId: tokenInfo.clientId,
|
|
138
|
-
subject: tokenInfo.subject,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
client.logger.warn("oauth2.introspect_failed", {
|
|
143
|
-
code: isOryError(err) ? err.code : "unknown",
|
|
144
|
-
message: err instanceof Error ? err.message : String(err),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
31
|
+
function createToolCallHandler(client) {
|
|
32
|
+
return async (event) => {
|
|
33
|
+
const decision = await (0, permission_js_1.decideToolPermission)({ tool: event.tool, input: event.input, threadID: event.thread.id }, client);
|
|
34
|
+
return decision.exitCode >= 2
|
|
35
|
+
? { action: "reject-and-continue", message: decision.reason ?? "Tool denied by Ory" }
|
|
36
|
+
: { action: "allow" };
|
|
37
|
+
};
|
|
147
38
|
}
|
|
148
|
-
// ─── tool.result ───────────────────────────────────────────────────
|
|
149
39
|
function createToolResultHandler(client) {
|
|
150
|
-
return
|
|
40
|
+
return (event, _ctx) => (0, argus_1.withHookContext)(client, { sessionId: event.thread?.id }, async () => {
|
|
151
41
|
const toolName = event.tool ?? "unknown";
|
|
152
|
-
const sessionId = event.thread?.id;
|
|
153
42
|
const hasError = event.status === "error";
|
|
154
|
-
client.tracer.setContext({
|
|
155
|
-
traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
|
|
156
|
-
sessionId,
|
|
157
|
-
});
|
|
158
43
|
client.logger.info("lifecycle.tool_result", {
|
|
159
44
|
toolName,
|
|
160
|
-
sessionId,
|
|
45
|
+
sessionId: event.thread?.id,
|
|
161
46
|
status: event.status,
|
|
162
47
|
hasError,
|
|
163
48
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
49
|
+
if (event.error)
|
|
50
|
+
client.logger.debug("lifecycle.tool_result.error", { error: event.error });
|
|
51
|
+
(0, argus_1.complete)(client, {
|
|
52
|
+
toolName,
|
|
53
|
+
input: event.input,
|
|
54
|
+
output: event.output,
|
|
55
|
+
status: hasError ? "error" : "ok",
|
|
56
|
+
extraActivityAttributes: {
|
|
167
57
|
status: event.status,
|
|
168
|
-
...(
|
|
169
|
-
...(0, argus_1.summarizeToolOutput)(toolName, event.output),
|
|
170
|
-
...(event.error ? { error: event.error } : {}),
|
|
58
|
+
...(event.error ? { hasError: true } : {}),
|
|
171
59
|
},
|
|
172
60
|
});
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
// ─── Helpers ───────────────────────────────────────────────────────
|
|
176
|
-
function isOryError(err) {
|
|
177
|
-
return (typeof err === "object" &&
|
|
178
|
-
err !== null &&
|
|
179
|
-
"code" in err &&
|
|
180
|
-
"message" in err);
|
|
61
|
+
});
|
|
181
62
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* A 10-second delegate timeout applies (a timed-out delegate is
|
|
14
14
|
* treated as a reject). See `AmpDelegateInput` below.
|
|
15
15
|
*
|
|
16
|
-
* 2. In-process plugin (SECONDARY,
|
|
16
|
+
* 2. In-process plugin (SECONDARY, activity auditing/auth) — a default-exported
|
|
17
17
|
* factory placed under `.amp/plugins/*.ts` that receives a `PluginAPI`
|
|
18
18
|
* (the `@ampcode/plugin` contract). Runs under Bun's TypeScript
|
|
19
19
|
* runtime. Used here ONLY for advisory session-start auth and
|
|
20
|
-
* post-tool
|
|
20
|
+
* post-tool activity logging; the blocking decision lives in the delegate
|
|
21
21
|
* helper for cleaner subprocess parity.
|
|
22
22
|
*
|
|
23
23
|
* We model the `@ampcode/plugin` API surface locally (the plugin package
|
|
@@ -133,7 +133,7 @@ export interface AmpAgentEndEvent {
|
|
|
133
133
|
/**
|
|
134
134
|
* Context (`PluginEventContext`) passed as the SECOND argument to every
|
|
135
135
|
* handler. Carries Amp's UI helpers, a shell runner (`$`), etc. Modeled as
|
|
136
|
-
* an open object — this plugin only authenticates and
|
|
136
|
+
* an open object — this plugin only authenticates and records activity, so it does
|
|
137
137
|
* not depend on any specific context member.
|
|
138
138
|
*/
|
|
139
139
|
export interface AmpPluginEventContext {
|
package/dist/types.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* A 10-second delegate timeout applies (a timed-out delegate is
|
|
15
15
|
* treated as a reject). See `AmpDelegateInput` below.
|
|
16
16
|
*
|
|
17
|
-
* 2. In-process plugin (SECONDARY,
|
|
17
|
+
* 2. In-process plugin (SECONDARY, activity auditing/auth) — a default-exported
|
|
18
18
|
* factory placed under `.amp/plugins/*.ts` that receives a `PluginAPI`
|
|
19
19
|
* (the `@ampcode/plugin` contract). Runs under Bun's TypeScript
|
|
20
20
|
* runtime. Used here ONLY for advisory session-start auth and
|
|
21
|
-
* post-tool
|
|
21
|
+
* post-tool activity logging; the blocking decision lives in the delegate
|
|
22
22
|
* helper for cleaner subprocess parity.
|
|
23
23
|
*
|
|
24
24
|
* We model the `@ampcode/plugin` API surface locally (the plugin package
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/amp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Ory plugin for Amp (Sourcegraph's coding agent): a permission delegate that authorizes every tool call plus an in-process plugin for session auth and
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ory plugin for Amp (Sourcegraph's coding agent): a permission delegate that authorizes every tool call plus an in-process plugin for session auth and activity auditing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|
|
7
7
|
"keywords": [
|
|
@@ -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
|
"devDependencies": {
|
|
74
76
|
"typescript": "^6.0.2",
|
|
@@ -82,7 +84,6 @@
|
|
|
82
84
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
83
85
|
"test": "vitest run",
|
|
84
86
|
"test:watch": "vitest",
|
|
85
|
-
"typecheck": "tsc --noEmit"
|
|
86
|
-
"dev": "node dist/dev/launcher.js"
|
|
87
|
+
"typecheck": "tsc --noEmit"
|
|
87
88
|
}
|
|
88
89
|
}
|