@ory/amp 0.14.0 → 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 -50
- package/dist/cli/main.js +22 -20
- 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 -72
- 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,72 +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
|
-
> No browser available (CI, SSH, headless)? The same walkthrough runs right in your terminal instead — or force it with `--no-web`.
|
|
31
|
-
|
|
32
|
-
That's it. Confirm everything landed with:
|
|
27
|
+
Confirm everything landed with:
|
|
33
28
|
|
|
34
29
|
```bash
|
|
35
30
|
npx -y -p @ory/amp ory-amp status
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, whether the 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 |
|
|
39
59
|
|
|
40
|
-
|
|
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.
|
|
41
63
|
|
|
42
64
|
## What you get
|
|
43
65
|
|
|
44
|
-
|
|
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:
|
|
45
69
|
|
|
46
|
-
- **Who's driving.** You sign in once in your browser; Amp gets its own identity
|
|
47
|
-
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **
|
|
48
|
-
- **A record of everything.** Every decision (allowed, denied, skipped) is logged as a trace you can send to 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.
|
|
49
72
|
|
|
50
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.
|
|
51
74
|
|
|
52
75
|
### See what's happening
|
|
53
76
|
|
|
54
|
-
Everything the plugin does is observable out of the box
|
|
77
|
+
Everything the plugin does is observable out of the box:
|
|
55
78
|
|
|
56
|
-
- **
|
|
57
|
-
- **Live dashboard.** `npx -y -p @ory/amp ory-amp dashboard` opens the same picture in your browser — configuration, identities, permission coverage, Ory service health, and the latest tool-call activity, all refreshing live. From there you can flip **enforcement** on or off, toggle **user login**, and use **Change stack** to reconnect to a different Ory — no CLI required.
|
|
58
|
-
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
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:
|
|
59
80
|
|
|
60
81
|
```bash
|
|
61
82
|
npx -y -p @ory/amp ory-amp watch
|
|
62
83
|
```
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
- **
|
|
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.
|
|
66
87
|
|
|
67
88
|
### Ready to enforce?
|
|
68
89
|
|
|
69
|
-
|
|
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.
|
|
70
91
|
|
|
71
92
|
```bash
|
|
72
|
-
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
|
|
73
94
|
```
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
Then a denied tool is actually blocked and Amp shows why.
|
|
76
97
|
|
|
77
98
|
<details>
|
|
78
99
|
<summary>How the plugin plugs into Amp</summary>
|
|
79
100
|
|
|
80
101
|
Amp gets the plugin in two pieces that work together:
|
|
81
102
|
|
|
82
|
-
- **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).
|
|
83
|
-
- **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`.
|
|
84
105
|
|
|
85
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.
|
|
86
107
|
|
|
@@ -88,38 +109,42 @@ The Ory skills land under `<project>/.agents/skills/`. `uninstall` removes the g
|
|
|
88
109
|
|
|
89
110
|
</details>
|
|
90
111
|
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
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).
|
|
94
|
-
|
|
95
|
-
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
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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` |
|
|
102
121
|
|
|
103
122
|
```bash
|
|
104
123
|
npx -y -p @ory/amp ory-amp configure \
|
|
105
124
|
--project-url https://<slug>.projects.oryapis.com \
|
|
106
|
-
--
|
|
125
|
+
--agent-security-url https://agents.console.ory.com
|
|
107
126
|
```
|
|
108
127
|
|
|
109
|
-
|
|
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.
|
|
110
135
|
|
|
111
|
-
|
|
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.
|
|
112
137
|
|
|
113
138
|
## Commands
|
|
114
139
|
|
|
115
140
|
```
|
|
116
|
-
ory-amp install | uninstall Install
|
|
141
|
+
ory-amp install | uninstall Install (add --project-url to also connect Agent Security) / remove
|
|
117
142
|
ory-amp status Show configuration, identities, permission coverage, and recent activity
|
|
118
|
-
ory-amp
|
|
119
|
-
ory-amp
|
|
120
|
-
ory-amp permissions <cmd> status | bootstrap | observe (watch) | enforce (block)
|
|
121
|
-
ory-amp configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --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
|
|
122
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)
|
|
123
148
|
```
|
|
124
149
|
|
|
125
150
|
All prefixed with `npx -y -p @ory/amp`.
|
|
@@ -127,16 +152,14 @@ All prefixed with `npx -y -p @ory/amp`.
|
|
|
127
152
|
## Troubleshooting
|
|
128
153
|
|
|
129
154
|
- **Local Ory fails to start** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
130
|
-
- **Browser sign-in loops** — reset with `ory-amp agent unregister` and try again.
|
|
131
|
-
- **
|
|
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.
|
|
132
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`.
|
|
133
|
-
- **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)).
|
|
134
158
|
|
|
135
159
|
## Learn more
|
|
136
160
|
|
|
137
161
|
- [Ory documentation](https://www.ory.com/docs/) · [Ory Console](https://console.ory.sh) · [Ory Elements](https://github.com/ory/elements)
|
|
138
162
|
- [Amp documentation](https://ampcode.com)
|
|
139
|
-
- [Repo README](../../README.md) and [AGENTS.md](../../AGENTS.md) — full env-var and permission-mode reference
|
|
140
163
|
|
|
141
164
|
## License
|
|
142
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,17 +99,11 @@ function main() {
|
|
|
91
99
|
process.exit(1);
|
|
92
100
|
});
|
|
93
101
|
break;
|
|
94
|
-
case "local":
|
|
95
|
-
(0, argus_1.runLocalCommand)("ory-amp", args).catch((err) => {
|
|
96
|
-
console.error(err.message ?? err);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
});
|
|
99
|
-
break;
|
|
100
102
|
case "watch":
|
|
101
|
-
(0, argus_1.runWatchCommand)("amp", args);
|
|
103
|
+
(0, argus_1.runWatchCommand)("ory-amp", "amp", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
102
104
|
break;
|
|
103
|
-
case "
|
|
104
|
-
(0, argus_1.
|
|
105
|
+
case "local":
|
|
106
|
+
(0, argus_1.runLocalCommand)("ory-amp", args).catch((err) => {
|
|
105
107
|
console.error(err.message ?? err);
|
|
106
108
|
process.exit(1);
|
|
107
109
|
});
|
|
@@ -162,13 +164,15 @@ async function status(args) {
|
|
|
162
164
|
const perms = Array.isArray(raw["amp.permissions"])
|
|
163
165
|
? raw["amp.permissions"]
|
|
164
166
|
: [];
|
|
165
|
-
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 ?? "")));
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
catch {
|
|
169
172
|
/* ignore parse errors */
|
|
170
173
|
}
|
|
171
|
-
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"))}`);
|
|
172
176
|
console.log(` In-process plugin: ${fs.existsSync(pluginFile) ? `installed (${pluginFile})` : "not installed"}`);
|
|
173
177
|
},
|
|
174
178
|
});
|
|
@@ -185,24 +189,22 @@ Commands:
|
|
|
185
189
|
uninstall [--project-dir <path>] Remove the Ory delegate, plugin, and skills
|
|
186
190
|
configure Set or view Ory project URL and API key
|
|
187
191
|
agent <status|unregister> Manage the agent's OAuth2 (DCR) identity
|
|
188
|
-
permissions
|
|
189
|
-
(status, bootstrap, observe, enforce)
|
|
192
|
+
permissions Show the live permission mode and per-tool coverage
|
|
190
193
|
setup [--project-dir <path>] Write config directly (fallback)
|
|
191
194
|
status [--project-dir <path>] Show plugin configuration and status
|
|
192
|
-
|
|
193
|
-
watch [trace-file] Tail the trace stream (OTel spans) live
|
|
195
|
+
watch [--json] [--lines <count>] Follow the live activity/debug log
|
|
194
196
|
version Show version and the ory-agent-plugins build commit
|
|
195
197
|
local <cmd> Manage local Ory dev environment
|
|
196
198
|
(up, down, status, seed, logs, env, configure, reset)
|
|
197
199
|
|
|
198
200
|
After installing, the Ory integration runs in two parts:
|
|
199
201
|
Permission delegate (ory-amp-permission) Authorize each tool call (blocking)
|
|
200
|
-
In-process plugin (.amp/plugins/ory.ts) Session auth +
|
|
202
|
+
In-process plugin (.amp/plugins/ory.ts) Session auth + activity auditing
|
|
201
203
|
|
|
202
204
|
Examples:
|
|
203
205
|
npx -y -p @ory/amp ory-amp install
|
|
204
|
-
npx -y -p @ory/amp ory-amp configure --project-url https://<slug>.projects.oryapis.com
|
|
205
|
-
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
|
|
206
208
|
npx -y -p @ory/amp ory-amp status
|
|
207
209
|
`);
|
|
208
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, the agent gate, and write the
|
|
10
|
-
* 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
|
-
* The user login runs every session and never blocks — it emits the
|
|
22
|
-
* user.auth audit span, refreshes tokens, and may prompt when interactive.
|
|
23
|
-
* Enforcement is governed solely by permissionMode at tool-call time.
|
|
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,104 +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, the agent gate, and write the
|
|
11
|
-
* 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
|
-
* The user login runs every session and never blocks — it emits the
|
|
23
|
-
* user.auth audit span, refreshes tokens, and may prompt when interactive.
|
|
24
|
-
* Enforcement is governed solely by permissionMode at tool-call time.
|
|
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. It runs every session and never blocks — the
|
|
59
|
-
// flow emits the user.auth audit span, refreshes tokens, and may
|
|
60
|
-
// prompt when interactive, but never prevents the session from
|
|
61
|
-
// starting. Enforcement is governed solely by permissionMode at
|
|
62
|
-
// tool-call time.
|
|
63
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
64
|
-
await userGate(client, {
|
|
23
|
+
await (0, argus_1.sessionStart)(client, {
|
|
24
|
+
harness: HARNESS,
|
|
65
25
|
binName: "ory-amp",
|
|
66
|
-
|
|
26
|
+
userLogin: deps.userLogin,
|
|
27
|
+
agentGate: deps.agentGate,
|
|
67
28
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
29
|
+
});
|
|
30
|
+
}
|
|
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" };
|
|
76
37
|
};
|
|
77
38
|
}
|
|
78
|
-
// ─── tool.result ───────────────────────────────────────────────────
|
|
79
39
|
function createToolResultHandler(client) {
|
|
80
|
-
return
|
|
40
|
+
return (event, _ctx) => (0, argus_1.withHookContext)(client, { sessionId: event.thread?.id }, async () => {
|
|
81
41
|
const toolName = event.tool ?? "unknown";
|
|
82
|
-
const sessionId = event.thread?.id;
|
|
83
42
|
const hasError = event.status === "error";
|
|
84
|
-
client.tracer.setContext({
|
|
85
|
-
traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
|
|
86
|
-
sessionId,
|
|
87
|
-
});
|
|
88
43
|
client.logger.info("lifecycle.tool_result", {
|
|
89
44
|
toolName,
|
|
90
|
-
sessionId,
|
|
45
|
+
sessionId: event.thread?.id,
|
|
91
46
|
status: event.status,
|
|
92
47
|
hasError,
|
|
93
48
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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: {
|
|
97
57
|
status: event.status,
|
|
98
|
-
...(
|
|
99
|
-
...(0, argus_1.summarizeToolOutput)(toolName, event.output),
|
|
100
|
-
...(event.error ? { error: event.error } : {}),
|
|
58
|
+
...(event.error ? { hasError: true } : {}),
|
|
101
59
|
},
|
|
102
60
|
});
|
|
103
|
-
};
|
|
61
|
+
});
|
|
104
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
|
}
|