@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 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
- **Security.** Amp runs real actions on your machine — editing files, running shell commands, calling APIs. The plugin gives every session a verifiable identity (you sign in once; Amp and any sub-agents it spawns each get their own), checks every tool call against permissions you control, and records each decision as an audit trace you can ship to your observability stack. It starts in watch mode so nothing is blocked on day one, and if Ory is ever unreachable it steps aside rather than locking you out.
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
- **Developer experience.** A single command installs the plugin and walks you through connecting — choose Ory Network, a local Docker stack, or audit-only, and it wires up the project, sign-in client, login, and permissions for you. It also helps you build Ory into your own app: ask in plain language to scaffold login, registration, and recovery pages, run a local Ory, or manage identities and permissions through the bundled MCP server.
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. It installs the plugin and walks you through connecting:
21
+ Run one command:
19
22
 
20
23
  ```bash
21
24
  npx -y -p @ory/amp ory-amp install
22
25
  ```
23
26
 
24
- It opens a guided setup **in your browser** where you pick how to connect — the default takes just a click:
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
- Re-run install with `--reconfigure` to change your connection later, or `--no-configure` to skip the wizard and configure by hand.
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
- Once connected, every tool Amp runs is governed by Ory three things happen automatically:
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 too, and registers it automatically on first run. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later.
47
- - **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **watch mode** — nothing is blocked, you just *see* what would be — so it never gets in your way on day one.
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 — no configuration required:
77
+ Everything the plugin does is observable out of the box:
55
78
 
56
- - **Status at a glance.** `npx -y -p @ory/amp ory-amp status` shows what's configured, who's signed in, how many built-in tools your permissions cover, and the most recent tool-call activity.
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
- Spans are also written to `~/.config/ory-agent-plugins/amp/ory-agent-trace.ndjson` (NDJSON, one span per line) — tail that file, or point `OTEL_EXPORTER_OTLP_ENDPOINT` at a collector to ship them straight to Jaeger, Honeycomb, or Grafana.
65
- - **Debug log.** For a verbose play-by-play, set `ORY_AGENT_DEBUG=true`; structured logs land in `~/.config/ory-agent-plugins/amp/ory-agent-debug.log`.
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
- When the watch-mode logs look right, turn on blocking with one command (setup already granted you the built-in tools):
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 enforce
93
+ npx -y -p @ory/amp ory-amp permissions # what the project grants, and the live mode
73
94
  ```
74
95
 
75
- Now a denied tool is actually blocked and Amp shows why. Go back to watch mode anytime with `permissions observe`. Prefer clicking? The dashboard has the same **Enforce** switch — flip it on, or back to watch mode, without touching the CLI. Use `permissions status` to see what's covered and `permissions bootstrap` to (re-)grant the built-in tools.
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). This is the piece that can actually block.
83
- - **An in-process plugin** at `<project>/.amp/plugins/ory.ts`, which Amp's built-in TypeScript runtime loads to sign you in at the start of a session and record what happened after each tool runs. It can't block from there, so it's advisory only — the gate above does the blocking.
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
- ## Also: add login to your own app
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
- The local Ory runs entirely on your laptop: the Ory APIs at `http://localhost:4000`, a login UI on `:4455` (not `:3000`, to avoid clashing with Next.js dev servers), the Ory Console on `:4100`, and a trace viewer (Jaeger) on `:16686`.
114
+ Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
98
115
 
99
- ## Configure by hand (CI / advanced)
100
-
101
- The guided setup covers most people. For scripted or CI setups, or to point at an existing Ory Network project, configure directly. 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.
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
- --oauth2-client-id <sign-in client id>
125
+ --agent-security-url https://agents.console.ory.com
107
126
  ```
108
127
 
109
- Amp's own identity registers itself automatically on first run nothing to create. The `--oauth2-client-id` is the one piece browser sign-in needs (the public client registered with the four loopback callback URLs `http://127.0.0.1:47823..47826/callback`); the guided setup makes it for you. Pass `--api-key ory_pat_...` only to override Amp's auto-created identity with a static token. For logging-only with no checks, use `--audit-only`. The interactive user login runs every session (always on, non-blocking) and only needs `ORY_OAUTH2_CLIENT_ID` to complete the browser flow. The matching env vars are `ORY_PROJECT_URL`, `ORY_OAUTH2_CLIENT_ID`, and `ORY_AGENT_API_KEY`.
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
- With nothing configured, the plugin still loads and runs in **pass-through mode**: skills and logging work, but no checks run and nothing is blocked. Perfectly fine if you only want the app-building features.
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/remove; --reconfigure re-runs setup, --no-web forces the terminal wizard, --no-configure skips it
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 dashboard Open the live browser dashboard (status + service health + Change stack)
119
- ory-amp watch Tail the live trace stream (OTel spans)
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
- - **Install ran but never asked how to connect** — you're almost certainly on a stale `npx` cache. `npx -p @ory/amp` (no version pin) reuses a previously-downloaded copy instead of re-resolving to the latest, so an older CLI whose install predates the setup wizard can run while you believe you're on the current release. 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.
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)().then(() => process.exit(0), (err) => {
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 "dashboard":
104
- (0, argus_1.runDashboardCommand)("ory-amp", "amp", args).then((code) => process.exit(code), (err) => {
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 (ory-amp-permission)" : "NOT 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 <cmd> Manage permission mode and tool 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
- dashboard Open the Ory Agent dashboard (system health + configuration)
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 + audit tracing
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 bootstrap
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
  }
@@ -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
- * to: "ory-amp-permission" }` — this is the blocking gate.
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 + audit tracing.
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
- * to: "ory-amp-permission" }` — this is the blocking gate.
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 + audit tracing.
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 DELEGATE_BIN = "ory-amp-permission";
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
- * Read the plugin's own version, walking up from dist/cli to the package
78
- * root. Used to version-pin the npx invocation written into the in-process
79
- * plugin loader and any delegate references.
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 readPackageVersion() {
82
- const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
83
- const raw = fs.readFileSync(pkgPath, "utf-8");
84
- const parsed = JSON.parse(raw);
85
- if (!parsed.version) {
86
- throw new Error(`Could not read version from ${pkgPath}`);
87
- }
88
- return parsed.version;
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: DELEGATE_BIN };
113
+ function delegateEntry(to) {
114
+ return { tool: "*", action: "delegate", to };
107
115
  }
108
116
  function isOryDelegate(entry) {
109
- return (typeof entry === "object" &&
110
- entry !== null &&
111
- entry.action === "delegate" &&
112
- entry.to === DELEGATE_BIN);
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 installed `@ory/amp` package's default-exported plugin
140
- * factory. Version-pinned in the header so the on-disk loader is traceable to
141
- * the package the settings were written against.
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(version) {
144
- return (`// Generated by ${PACKAGE_NAME}@${version} — do not edit by hand.\n` +
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 "${PACKAGE_NAME}";\n`);
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
- console.log(JSON.stringify({ "amp.permissions": [delegateEntry()] }, null, 2));
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 + tracing).
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(version));
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: ${DELEGATE_BIN} (tool: "*", action: 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)().then(() => process.exit(0), (err) => {
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
  });
@@ -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>;
@@ -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
- client.tracer.setContext({
97
- traceId: (0, argus_1.deriveTraceId)(sessionId ?? toolName),
98
- sessionId,
99
- });
100
- client.logger.info("delegate.received", { toolName, sessionId });
101
- // Audit-only mode: never block, just record the invocation.
102
- if ((0, argus_1.resolveConfig)().auditOnly) {
103
- client.tracer.record("tool.invoke", "ok", {
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
- code: decision.error.code,
146
- message: "Ory unreachable or errored, failing open",
147
- });
148
- return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
149
- }
150
- if (decision.kind === "allow") {
151
- client.tracer.record("tool.invoke", "ok", {
152
- attributes: { ...attrs, ...decisionAttrs, allowed: true },
153
- });
154
- return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
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
- if (decision.kind === "observe") {
157
- // Observe mode: log the would-be denial but let the tool through.
158
- client.tracer.record("tool.block", "denied", {
159
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
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
- // decision.kind === "deny" → enforce mode hard block.
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 client.tracer.shutdown();
204
+ await flushEvents();
239
205
  process.exit(exitCode);
240
206
  }
241
- // Only run the stdin→exit-code pipeline when invoked as the CLI entry
242
- // point. Tests import this module to call `decideToolPermission` directly,
243
- // so guarding here keeps `main()` (and its `process.exit`) from firing on
244
- // import. `module` exists under CJS output (module: nodenext → CJS).
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
- * Create the Amp in-process plugin factory.
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: "amp" })
41
- : argus_1.OryAgentClient.fromEnv("amp");
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 async (event, _ctx) => {
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
- client.tracer.setContext({
52
- traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
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
- harness: "amp",
26
+ userLogin: deps.userLogin,
27
+ agentGate: deps.agentGate,
67
28
  });
68
- // Resolve the agent identity (machine credentials). Never blocks;
69
- // attaches the agent's bearer token to outgoing Ory API calls.
70
- const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
71
- await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "amp" });
72
- // Once both principals are populated, write the user→agent delegation
73
- // tuple. Written at most once per install + fail-open: audit-trail data
74
- // only.
75
- await (0, argus_1.writeUserDelegatesAgent)(client);
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 async (event, _ctx) => {
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
- client.tracer.record("tool.complete", hasError ? "error" : "ok", {
95
- attributes: {
96
- toolName,
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
- ...(0, argus_1.summarizeToolInput)(toolName, event.input),
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, tracing/auth) — a default-exported
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 tracing; the blocking decision lives in the delegate
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 traces, so it does
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, tracing/auth) — a default-exported
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 tracing; the blocking decision lives in the delegate
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.14.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 audit tracing",
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
- "tracing",
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
- "reo-census": "^1.2.8",
71
- "@ory/argus": "0.14.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
  }