@ory/amp 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/cli/assets.d.ts +18 -0
- package/dist/cli/assets.js +91 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +192 -0
- package/dist/cli/setup.d.ts +32 -0
- package/dist/cli/setup.js +197 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +15 -0
- package/dist/permission.d.ts +46 -0
- package/dist/permission.js +201 -0
- package/dist/plugin.d.ts +42 -0
- package/dist/plugin.js +217 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +45 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractToolName = exports.decideToolPermission = exports.createOryPlugin = void 0;
|
|
4
|
+
const plugin_js_1 = require("./plugin.js");
|
|
5
|
+
Object.defineProperty(exports, "createOryPlugin", { enumerable: true, get: function () { return plugin_js_1.createOryPlugin; } });
|
|
6
|
+
// Re-export the delegate decision logic for programmatic use / testing.
|
|
7
|
+
var permission_js_1 = require("./permission.js");
|
|
8
|
+
Object.defineProperty(exports, "decideToolPermission", { enumerable: true, get: function () { return permission_js_1.decideToolPermission; } });
|
|
9
|
+
Object.defineProperty(exports, "extractToolName", { enumerable: true, get: function () { return permission_js_1.extractToolName; } });
|
|
10
|
+
// Amp loads in-process plugins via import() and expects a default-exported
|
|
11
|
+
// plugin function `(api) => void`. CJS output (module: nodenext → CJS)
|
|
12
|
+
// surfaces `module.exports` as the ESM `default`, so a single default
|
|
13
|
+
// export is what Amp's loader receives. Instantiate the plugin from env.
|
|
14
|
+
// eslint-disable-next-line import/no-default-export
|
|
15
|
+
exports.default = (0, plugin_js_1.createOryPlugin)();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Amp permission delegate helper (PRIMARY blocking gate).
|
|
4
|
+
*
|
|
5
|
+
* Amp's `amp.permissions` config can route a tool to
|
|
6
|
+
* `{ action: "delegate", to: "ory-amp-permission" }`. Amp then runs this
|
|
7
|
+
* program as a subprocess for each matching tool call, passing the tool
|
|
8
|
+
* parameters as JSON on stdin and reading the decision from the exit code:
|
|
9
|
+
*
|
|
10
|
+
* exit 0 = allow
|
|
11
|
+
* exit 1 = ask the user (RESERVED — not produced by this version)
|
|
12
|
+
* exit ≥2 = reject; stderr is forwarded to the model as the reason
|
|
13
|
+
*
|
|
14
|
+
* NOTE the inversion vs. the other harnesses' subprocess hooks (where
|
|
15
|
+
* exit 2 blocks but the allow path is exit 0 with structured stdout):
|
|
16
|
+
* here a denial is exit 2 *plus a stderr message*, and stdout is unused.
|
|
17
|
+
*
|
|
18
|
+
* TIMEOUT: Amp enforces a 10-second delegate timeout. A delegate that does
|
|
19
|
+
* not exit within 10s is treated as a reject, so every Ory call below
|
|
20
|
+
* (session resolution + permission check) must complete well inside that
|
|
21
|
+
* window; the fail-open arms below keep slow/unreachable Ory from wedging
|
|
22
|
+
* the agent, but a hard 10s hang would still surface as a reject.
|
|
23
|
+
*
|
|
24
|
+
* Fail-open: any error — parse failure, network error, rate limit, crash —
|
|
25
|
+
* resolves to exit 0 (allow). The delegate must never wedge the agent.
|
|
26
|
+
*
|
|
27
|
+
* Verified against the installed `amp` binary (delegate exit-code contract
|
|
28
|
+
* and 10s timeout).
|
|
29
|
+
*/
|
|
30
|
+
import { OryAgentClient } from "@ory/argus";
|
|
31
|
+
import { type AmpDelegateInput } from "./types.js";
|
|
32
|
+
export interface DelegateDecision {
|
|
33
|
+
/** Process exit code to return to Amp. */
|
|
34
|
+
exitCode: number;
|
|
35
|
+
/** Reason to write to stderr (only set when rejecting). */
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Pull the tool name out of the (undocumented) delegate payload. */
|
|
39
|
+
export declare function extractToolName(input: AmpDelegateInput): string;
|
|
40
|
+
/**
|
|
41
|
+
* Core decision logic, factored out so tests can call it directly without
|
|
42
|
+
* spawning a subprocess or hitting `process.exit`. Resolves to the exit
|
|
43
|
+
* code (and stderr reason) Amp expects. Always fails open: any thrown
|
|
44
|
+
* error resolves to ALLOW.
|
|
45
|
+
*/
|
|
46
|
+
export declare function decideToolPermission(input: AmpDelegateInput, client: OryAgentClient): Promise<DelegateDecision>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Amp permission delegate helper (PRIMARY blocking gate).
|
|
5
|
+
*
|
|
6
|
+
* Amp's `amp.permissions` config can route a tool to
|
|
7
|
+
* `{ action: "delegate", to: "ory-amp-permission" }`. Amp then runs this
|
|
8
|
+
* program as a subprocess for each matching tool call, passing the tool
|
|
9
|
+
* parameters as JSON on stdin and reading the decision from the exit code:
|
|
10
|
+
*
|
|
11
|
+
* exit 0 = allow
|
|
12
|
+
* exit 1 = ask the user (RESERVED — not produced by this version)
|
|
13
|
+
* exit ≥2 = reject; stderr is forwarded to the model as the reason
|
|
14
|
+
*
|
|
15
|
+
* NOTE the inversion vs. the other harnesses' subprocess hooks (where
|
|
16
|
+
* exit 2 blocks but the allow path is exit 0 with structured stdout):
|
|
17
|
+
* here a denial is exit 2 *plus a stderr message*, and stdout is unused.
|
|
18
|
+
*
|
|
19
|
+
* TIMEOUT: Amp enforces a 10-second delegate timeout. A delegate that does
|
|
20
|
+
* not exit within 10s is treated as a reject, so every Ory call below
|
|
21
|
+
* (session resolution + permission check) must complete well inside that
|
|
22
|
+
* window; the fail-open arms below keep slow/unreachable Ory from wedging
|
|
23
|
+
* the agent, but a hard 10s hang would still surface as a reject.
|
|
24
|
+
*
|
|
25
|
+
* Fail-open: any error — parse failure, network error, rate limit, crash —
|
|
26
|
+
* resolves to exit 0 (allow). The delegate must never wedge the agent.
|
|
27
|
+
*
|
|
28
|
+
* Verified against the installed `amp` binary (delegate exit-code contract
|
|
29
|
+
* and 10s timeout).
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.extractToolName = extractToolName;
|
|
33
|
+
exports.decideToolPermission = decideToolPermission;
|
|
34
|
+
const argus_1 = require("@ory/argus");
|
|
35
|
+
const types_js_1 = require("./types.js");
|
|
36
|
+
function resolveNamespace() {
|
|
37
|
+
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
38
|
+
}
|
|
39
|
+
/** Pull the tool name out of the (undocumented) delegate payload. */
|
|
40
|
+
function extractToolName(input) {
|
|
41
|
+
return input.tool ?? input.toolName ?? input.name ?? "unknown";
|
|
42
|
+
}
|
|
43
|
+
/** Pull the session/thread id out of the delegate payload, if present. */
|
|
44
|
+
function extractSessionId(input) {
|
|
45
|
+
return input.threadID ?? input.sessionID ?? input.session_id;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Core decision logic, factored out so tests can call it directly without
|
|
49
|
+
* spawning a subprocess or hitting `process.exit`. Resolves to the exit
|
|
50
|
+
* code (and stderr reason) Amp expects. Always fails open: any thrown
|
|
51
|
+
* error resolves to ALLOW.
|
|
52
|
+
*/
|
|
53
|
+
async function decideToolPermission(input, client) {
|
|
54
|
+
const toolName = extractToolName(input);
|
|
55
|
+
const sessionId = extractSessionId(input);
|
|
56
|
+
client.tracer.setContext({
|
|
57
|
+
traceId: (0, argus_1.deriveTraceId)(sessionId ?? toolName),
|
|
58
|
+
sessionId,
|
|
59
|
+
});
|
|
60
|
+
client.logger.info("delegate.received", { toolName, sessionId });
|
|
61
|
+
// Audit-only mode: never block, just record the invocation.
|
|
62
|
+
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
63
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
64
|
+
attributes: { toolName, mode: "audit-only" },
|
|
65
|
+
});
|
|
66
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
67
|
+
}
|
|
68
|
+
const subject = (0, argus_1.resolveUserSubject)(client, sessionId ? `session:${sessionId}` : undefined);
|
|
69
|
+
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
70
|
+
try {
|
|
71
|
+
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
72
|
+
harness: "amp",
|
|
73
|
+
toolName,
|
|
74
|
+
check: {
|
|
75
|
+
namespace: resolveNamespace(),
|
|
76
|
+
object: toolName,
|
|
77
|
+
relation: "use",
|
|
78
|
+
...subject,
|
|
79
|
+
},
|
|
80
|
+
spanAttributes: { toolName },
|
|
81
|
+
});
|
|
82
|
+
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
83
|
+
// the user.interaction span is already recorded; allow so Amp can
|
|
84
|
+
// surface the prompt to the user.
|
|
85
|
+
if (outcome.kind === "interactive") {
|
|
86
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
87
|
+
}
|
|
88
|
+
const decision = outcome;
|
|
89
|
+
const attrs = { toolName };
|
|
90
|
+
const decisionAttrs = decision.spanAttributes;
|
|
91
|
+
if (decision.kind === "fail_open") {
|
|
92
|
+
// network_error / rate_limited / unknown → allow.
|
|
93
|
+
client.logger.warn("delegate.fail_open", {
|
|
94
|
+
toolName,
|
|
95
|
+
code: decision.error.code,
|
|
96
|
+
message: "Ory unreachable or errored, failing open",
|
|
97
|
+
});
|
|
98
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
99
|
+
}
|
|
100
|
+
if (decision.kind === "allow") {
|
|
101
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
102
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
103
|
+
});
|
|
104
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
105
|
+
}
|
|
106
|
+
if (decision.kind === "observe") {
|
|
107
|
+
// Observe mode: log the would-be denial but let the tool through.
|
|
108
|
+
client.tracer.record("tool.block", "denied", {
|
|
109
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
110
|
+
});
|
|
111
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
112
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
113
|
+
});
|
|
114
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
115
|
+
}
|
|
116
|
+
// decision.kind === "deny" → enforce mode hard block.
|
|
117
|
+
client.tracer.record("tool.block", "denied", {
|
|
118
|
+
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
119
|
+
});
|
|
120
|
+
const reason = (0, argus_1.formatDenialMessage)({
|
|
121
|
+
tool: toolName,
|
|
122
|
+
subjectId,
|
|
123
|
+
namespace: resolveNamespace(),
|
|
124
|
+
});
|
|
125
|
+
client.logger.warn("tool.denied", { toolName, subjectId, message: reason });
|
|
126
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.REJECT, reason };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Fail open on any unexpected error.
|
|
130
|
+
client.logger.error("delegate.error", {
|
|
131
|
+
toolName,
|
|
132
|
+
message: err instanceof Error ? err.message : String(err),
|
|
133
|
+
});
|
|
134
|
+
return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Read all of stdin as a string.
|
|
139
|
+
*
|
|
140
|
+
* Uses event listeners instead of `for await` so we don't depend on the
|
|
141
|
+
* parent closing stdin (sending EOF). If no EOF arrives, we resolve after
|
|
142
|
+
* a short idle gap once data has been received. Copied from the gemini-cli
|
|
143
|
+
* subprocess hook for robustness parity.
|
|
144
|
+
*/
|
|
145
|
+
function readStdin() {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
let resolved = false;
|
|
149
|
+
function done() {
|
|
150
|
+
if (!resolved) {
|
|
151
|
+
resolved = true;
|
|
152
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let idleTimer;
|
|
156
|
+
process.stdin.on("data", (chunk) => {
|
|
157
|
+
chunks.push(chunk);
|
|
158
|
+
clearTimeout(idleTimer);
|
|
159
|
+
idleTimer = setTimeout(done, 100);
|
|
160
|
+
});
|
|
161
|
+
process.stdin.on("end", done);
|
|
162
|
+
process.stdin.on("error", (err) => {
|
|
163
|
+
if (!resolved) {
|
|
164
|
+
resolved = true;
|
|
165
|
+
reject(err);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function main() {
|
|
171
|
+
const client = argus_1.OryAgentClient.fromEnv("amp");
|
|
172
|
+
const raw = await readStdin();
|
|
173
|
+
let input;
|
|
174
|
+
try {
|
|
175
|
+
input = raw.trim() ? JSON.parse(raw) : {};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Parse failure — fail open (allow) rather than wedging the agent.
|
|
179
|
+
client.logger.error("delegate.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
180
|
+
await client.tracer.shutdown();
|
|
181
|
+
process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
|
|
182
|
+
}
|
|
183
|
+
const { exitCode, reason } = await decideToolPermission(input, client);
|
|
184
|
+
if (exitCode === types_js_1.AMP_DELEGATE_EXIT.REJECT && reason) {
|
|
185
|
+
// stderr is forwarded to the model as the rejection reason.
|
|
186
|
+
process.stderr.write(reason + "\n");
|
|
187
|
+
}
|
|
188
|
+
await client.tracer.shutdown();
|
|
189
|
+
process.exit(exitCode);
|
|
190
|
+
}
|
|
191
|
+
// Only run the stdin→exit-code pipeline when invoked as the CLI entry
|
|
192
|
+
// point. Tests import this module to call `decideToolPermission` directly,
|
|
193
|
+
// so guarding here keeps `main()` (and its `process.exit`) from firing on
|
|
194
|
+
// import. `module` exists under CJS output (module: nodenext → CJS).
|
|
195
|
+
if (require.main === module) {
|
|
196
|
+
main().catch((err) => {
|
|
197
|
+
// Top-level safety net: never block on an unexpected crash.
|
|
198
|
+
process.stderr.write(`[ory-agent] delegate fatal: ${err}\n`);
|
|
199
|
+
process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
|
|
200
|
+
});
|
|
201
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
|
|
3
|
+
*
|
|
4
|
+
* Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
|
|
5
|
+
* calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
|
|
6
|
+
* Each handler receives the event payload and a `PluginEventContext`. We
|
|
7
|
+
* subscribe to:
|
|
8
|
+
*
|
|
9
|
+
* session.start → run the user gate (advisory, allowBlock:false), the
|
|
10
|
+
* agent gate, and write the user→agent delegation tuple.
|
|
11
|
+
* tool.result → record a `tool.complete` audit span.
|
|
12
|
+
*
|
|
13
|
+
* The BLOCKING permission decision deliberately does NOT live here. Amp's
|
|
14
|
+
* in-process `tool.call` event *can* block (via a `reject-and-continue`
|
|
15
|
+
* result), but we route blocking through the standalone permission delegate
|
|
16
|
+
* helper (`permission.ts`) instead — that gives the same subprocess parity
|
|
17
|
+
* as the other harnesses and keeps the in-process module purely
|
|
18
|
+
* observational. The delegate runs as its own process and decides by exit
|
|
19
|
+
* code; this module only authenticates and traces.
|
|
20
|
+
*
|
|
21
|
+
* Because Amp's `session.start` handler has no return-value channel for
|
|
22
|
+
* blocking, the user login runs in advisory mode (allowBlock:false), like
|
|
23
|
+
* the OpenCode / OpenClaw factory plugins.
|
|
24
|
+
*/
|
|
25
|
+
import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
|
|
26
|
+
import type { AmpPlugin } from "./types.js";
|
|
27
|
+
export interface CreateOryPluginDeps {
|
|
28
|
+
/** Test injection point for the user login flow. */
|
|
29
|
+
userLogin?: typeof ensureUserAuthenticated;
|
|
30
|
+
/** Test injection point for the agent identity gate. */
|
|
31
|
+
agentGate?: typeof ensureAgentIdentity;
|
|
32
|
+
}
|
|
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
|
+
export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
|
|
40
|
+
projectUrl: string;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
}, deps?: CreateOryPluginDeps): AmpPlugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
|
|
4
|
+
*
|
|
5
|
+
* Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
|
|
6
|
+
* calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
|
|
7
|
+
* Each handler receives the event payload and a `PluginEventContext`. We
|
|
8
|
+
* subscribe to:
|
|
9
|
+
*
|
|
10
|
+
* session.start → run the user gate (advisory, allowBlock:false), the
|
|
11
|
+
* agent gate, and write the user→agent delegation tuple.
|
|
12
|
+
* tool.result → record a `tool.complete` audit span.
|
|
13
|
+
*
|
|
14
|
+
* The BLOCKING permission decision deliberately does NOT live here. Amp's
|
|
15
|
+
* in-process `tool.call` event *can* block (via a `reject-and-continue`
|
|
16
|
+
* result), but we route blocking through the standalone permission delegate
|
|
17
|
+
* helper (`permission.ts`) instead — that gives the same subprocess parity
|
|
18
|
+
* as the other harnesses and keeps the in-process module purely
|
|
19
|
+
* observational. The delegate runs as its own process and decides by exit
|
|
20
|
+
* code; this module only authenticates and traces.
|
|
21
|
+
*
|
|
22
|
+
* Because Amp's `session.start` handler has no return-value channel for
|
|
23
|
+
* blocking, the user login runs in advisory mode (allowBlock:false), like
|
|
24
|
+
* the OpenCode / OpenClaw factory plugins.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.createOryPlugin = createOryPlugin;
|
|
28
|
+
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
|
+
*/
|
|
35
|
+
function createOryPlugin(clientOrConfig, deps = {}) {
|
|
36
|
+
return (api) => {
|
|
37
|
+
const client = clientOrConfig instanceof argus_1.OryAgentClient
|
|
38
|
+
? clientOrConfig
|
|
39
|
+
: clientOrConfig
|
|
40
|
+
? new argus_1.OryAgentClient({ ...clientOrConfig, harness: "amp" })
|
|
41
|
+
: argus_1.OryAgentClient.fromEnv("amp");
|
|
42
|
+
api.on("session.start", createSessionStartHandler(client, deps));
|
|
43
|
+
api.on("tool.result", createToolResultHandler(client));
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ─── session.start ─────────────────────────────────────────────────
|
|
47
|
+
function createSessionStartHandler(client, deps) {
|
|
48
|
+
return async (event, _ctx) => {
|
|
49
|
+
const sessionId = event.thread?.id;
|
|
50
|
+
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. Amp's session.start has no block primitive, so
|
|
59
|
+
// allowBlock is false — the flow emits the user.auth audit span,
|
|
60
|
+
// refreshes tokens, and may prompt when interactive, but never
|
|
61
|
+
// prevents the session from starting. When ORY_USER_LOGIN is unset it
|
|
62
|
+
// is a no-op (mode === "disabled") and we fall through to legacy
|
|
63
|
+
// logging.
|
|
64
|
+
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
65
|
+
const decision = await userGate(client, {
|
|
66
|
+
binName: "ory-amp",
|
|
67
|
+
harness: "amp",
|
|
68
|
+
allowBlock: false,
|
|
69
|
+
});
|
|
70
|
+
// Resolve the agent identity (machine credentials). Never blocks;
|
|
71
|
+
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
72
|
+
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
73
|
+
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "amp" });
|
|
74
|
+
// Once both principals are populated, write the user→agent delegation
|
|
75
|
+
// tuple. Idempotent + fail-open: audit-trail data only.
|
|
76
|
+
await recordUserDelegatesAgent(client);
|
|
77
|
+
if (decision.mode !== "disabled") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const resolved = (0, argus_1.resolveConfig)();
|
|
81
|
+
if (resolved.auditOnly) {
|
|
82
|
+
client.logger.info("config.audit_only", {
|
|
83
|
+
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!resolved.projectUrl) {
|
|
88
|
+
client.logger.warn("config.not_configured", {
|
|
89
|
+
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
90
|
+
"Run 'npx ory-amp configure' to connect to an Ory project.",
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
95
|
+
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
96
|
+
if (sessionToken) {
|
|
97
|
+
await verifySessionToken(client, sessionToken);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (oauth2Token) {
|
|
101
|
+
await verifyOAuth2Token(client, oauth2Token);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
client.logger.warn("session.no_credentials", {
|
|
105
|
+
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
106
|
+
"Skipping authentication.",
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function verifySessionToken(client, token) {
|
|
111
|
+
try {
|
|
112
|
+
const session = await client.verifySession(token);
|
|
113
|
+
if (!session.active) {
|
|
114
|
+
client.logger.warn("session.inactive", {
|
|
115
|
+
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
client.logger.warn("session.verify_failed", {
|
|
121
|
+
code: isOryError(err) ? err.code : "unknown",
|
|
122
|
+
message: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function verifyOAuth2Token(client, token) {
|
|
127
|
+
try {
|
|
128
|
+
const tokenInfo = await client.introspectToken(token);
|
|
129
|
+
if (!tokenInfo.active) {
|
|
130
|
+
client.logger.warn("oauth2.token_inactive", {
|
|
131
|
+
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
client.logger.info("oauth2.session_authenticated", {
|
|
136
|
+
clientId: tokenInfo.clientId,
|
|
137
|
+
subject: tokenInfo.subject,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
client.logger.warn("oauth2.introspect_failed", {
|
|
142
|
+
code: isOryError(err) ? err.code : "unknown",
|
|
143
|
+
message: err instanceof Error ? err.message : String(err),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ─── tool.result ───────────────────────────────────────────────────
|
|
148
|
+
function createToolResultHandler(client) {
|
|
149
|
+
return async (event, _ctx) => {
|
|
150
|
+
const toolName = event.tool ?? "unknown";
|
|
151
|
+
const sessionId = event.thread?.id;
|
|
152
|
+
const hasError = event.status === "error";
|
|
153
|
+
client.tracer.setContext({
|
|
154
|
+
traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
|
|
155
|
+
sessionId,
|
|
156
|
+
});
|
|
157
|
+
client.logger.info("lifecycle.tool_result", {
|
|
158
|
+
toolName,
|
|
159
|
+
sessionId,
|
|
160
|
+
status: event.status,
|
|
161
|
+
hasError,
|
|
162
|
+
});
|
|
163
|
+
client.tracer.record("tool.complete", hasError ? "error" : "ok", {
|
|
164
|
+
attributes: {
|
|
165
|
+
toolName,
|
|
166
|
+
status: event.status,
|
|
167
|
+
...(0, argus_1.summarizeToolInput)(toolName, event.input),
|
|
168
|
+
...(0, argus_1.summarizeToolOutput)(toolName, event.output),
|
|
169
|
+
...(event.error ? { error: event.error } : {}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
175
|
+
function resolveNamespace() {
|
|
176
|
+
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
177
|
+
}
|
|
178
|
+
function isOryError(err) {
|
|
179
|
+
return (typeof err === "object" &&
|
|
180
|
+
err !== null &&
|
|
181
|
+
"code" in err &&
|
|
182
|
+
"message" in err);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Write the user→agent delegation tuple. Idempotent and fail-open:
|
|
186
|
+
* requires both principal subjects to be populated; any error (including
|
|
187
|
+
* an unconfigured projectUrl, which manifests as a network_error) is
|
|
188
|
+
* logged and swallowed. Purely audit-trail data — does not affect
|
|
189
|
+
* enforcement.
|
|
190
|
+
*/
|
|
191
|
+
async function recordUserDelegatesAgent(client) {
|
|
192
|
+
const user = client.userPrincipal.subject;
|
|
193
|
+
const agent = client.agentPrincipal.subject;
|
|
194
|
+
if (!user || !agent) {
|
|
195
|
+
client.logger.debug("delegation.skip", {
|
|
196
|
+
reason: "missing principal",
|
|
197
|
+
hasUser: !!user,
|
|
198
|
+
hasAgent: !!agent,
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await client.createRelationship({
|
|
204
|
+
namespace: resolveNamespace(),
|
|
205
|
+
object: `agent:${agent}`,
|
|
206
|
+
relation: "delegate",
|
|
207
|
+
subjectId: `user:${user}`,
|
|
208
|
+
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const oryErr = err;
|
|
212
|
+
client.logger.warn("delegation.user_to_agent.failed", {
|
|
213
|
+
code: oryErr.code,
|
|
214
|
+
message: oryErr.message,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|