@ory/argus 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 +31 -46
- package/assets/commands/temporal-up.md +1 -1
- package/assets/skills/auth-setup/SKILL.md +1 -1
- package/assets/skills/local-dev/SKILL.md +17 -7
- package/assets/skills/ory-build-agent/SKILL.md +39 -91
- package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
- package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
- package/assets/skills/permissions-onboarding/SKILL.md +131 -104
- package/dist/adapters.d.ts +88 -32
- package/dist/adapters.js +443 -164
- package/dist/agent-auth.d.ts +226 -67
- package/dist/agent-auth.js +951 -205
- package/dist/auth-store.d.ts +37 -2
- package/dist/auth-store.js +37 -3
- package/dist/auth.d.ts +33 -4
- package/dist/auth.js +163 -20
- package/dist/bash-parser.d.ts +98 -0
- package/dist/bash-parser.js +396 -0
- package/dist/branding.d.ts +77 -16
- package/dist/branding.js +93 -23
- package/dist/build-info.json +4 -4
- package/dist/cli-invocation.d.ts +1 -1
- package/dist/cli-invocation.js +2 -1
- package/dist/cli.d.ts +20 -29
- package/dist/cli.js +271 -239
- package/dist/client.d.ts +175 -138
- package/dist/client.js +672 -391
- package/dist/config.d.ts +217 -54
- package/dist/config.js +461 -42
- package/dist/context.d.ts +10 -0
- package/dist/context.js +21 -0
- package/dist/contract-suite.d.ts +3 -5
- package/dist/contract-suite.js +75 -47
- package/dist/denial.d.ts +36 -3
- package/dist/denial.js +79 -10
- package/dist/event-reporter.d.ts +77 -0
- package/dist/event-reporter.js +776 -0
- package/dist/external-registrations-main.d.ts +10 -0
- package/dist/external-registrations-main.js +38 -0
- package/dist/external-registrations.d.ts +79 -0
- package/dist/external-registrations.js +188 -0
- package/dist/help-cli.d.ts +39 -0
- package/dist/help-cli.js +55 -0
- package/dist/hook-timeout.d.ts +64 -0
- package/dist/hook-timeout.js +88 -0
- package/dist/index.d.ts +28 -23
- package/dist/index.js +173 -58
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +38 -6
- package/dist/local/cli.js +11 -6
- package/dist/local/configs.d.ts +74 -18
- package/dist/local/configs.js +291 -84
- package/dist/local/health.js +7 -7
- package/dist/local/index.d.ts +2 -2
- package/dist/local/index.js +24 -10
- package/dist/local/manager.d.ts +20 -1
- package/dist/local/manager.js +159 -36
- package/dist/local/ports.d.ts +158 -0
- package/dist/local/ports.js +443 -0
- package/dist/local/seed.d.ts +13 -5
- package/dist/local/seed.js +62 -38
- package/dist/logger.d.ts +54 -25
- package/dist/logger.js +329 -63
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +10 -5
- package/dist/mirror-bootstrap.d.ts +48 -0
- package/dist/mirror-bootstrap.js +254 -0
- package/dist/opl.d.ts +289 -0
- package/dist/opl.js +446 -0
- package/dist/permission-mode.d.ts +87 -0
- package/dist/permission-mode.js +307 -0
- package/dist/permissions-cli.d.ts +13 -49
- package/dist/permissions-cli.js +154 -348
- package/dist/permissions.d.ts +148 -38
- package/dist/permissions.js +591 -45
- package/dist/post-install.d.ts +33 -0
- package/dist/post-install.js +127 -0
- package/dist/read-credential.d.ts +65 -0
- package/dist/read-credential.js +86 -0
- package/dist/registry/cli.js +5 -2
- package/dist/registry/config.d.ts +0 -17
- package/dist/registry/config.js +0 -23
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +2 -2
- package/dist/registry/manager.d.ts +4 -21
- package/dist/registry/manager.js +21 -37
- package/dist/runtime-credential.d.ts +140 -0
- package/dist/runtime-credential.js +572 -0
- package/dist/runtime.d.ts +408 -0
- package/dist/runtime.js +748 -0
- package/dist/setup.d.ts +23 -47
- package/dist/setup.js +59 -116
- package/dist/skills.js +0 -7
- package/dist/status-cli.d.ts +27 -11
- package/dist/status-cli.js +121 -115
- package/dist/status-data.d.ts +116 -17
- package/dist/status-data.js +121 -38
- package/dist/subject.d.ts +126 -20
- package/dist/subject.js +215 -30
- package/dist/testing.d.ts +74 -38
- package/dist/testing.js +185 -68
- package/dist/tool-catalog.d.ts +53 -11
- package/dist/tool-catalog.js +164 -13
- package/dist/tool-metadata.d.ts +7 -6
- package/dist/tool-metadata.js +6 -5
- package/dist/types.d.ts +11 -1
- package/dist/uninstall.d.ts +71 -34
- package/dist/uninstall.js +217 -85
- package/dist/user-login.d.ts +9 -10
- package/dist/user-login.js +56 -75
- package/dist/watch-cli.d.ts +6 -0
- package/dist/watch-cli.js +217 -0
- package/package.json +4 -27
- package/assets/commands/dashboard.md +0 -34
- package/dist/dashboard-cli.d.ts +0 -8
- package/dist/dashboard-cli.js +0 -70
- package/dist/dev.d.ts +0 -103
- package/dist/dev.js +0 -583
- package/dist/interactive-setup.d.ts +0 -286
- package/dist/interactive-setup.js +0 -1734
- package/dist/local/jaeger-main.d.ts +0 -13
- package/dist/local/jaeger-main.js +0 -85
- package/dist/local/jaeger.d.ts +0 -50
- package/dist/local/jaeger.js +0 -162
- package/dist/otel/exporter.d.ts +0 -17
- package/dist/otel/exporter.js +0 -12
- package/dist/otel/index.d.ts +0 -2
- package/dist/otel/index.js +0 -8
- package/dist/otel/otlp.d.ts +0 -103
- package/dist/otel/otlp.js +0 -385
- package/dist/project-api-key.d.ts +0 -69
- package/dist/project-api-key.js +0 -147
- package/dist/setup-actions.d.ts +0 -232
- package/dist/setup-actions.js +0 -507
- package/dist/tracer.d.ts +0 -190
- package/dist/tracer.js +0 -481
- package/dist/watch-sandbox.d.ts +0 -9
- package/dist/watch-sandbox.js +0 -81
- package/dist/web/api.d.ts +0 -33
- package/dist/web/api.js +0 -294
- package/dist/web/launch.d.ts +0 -11
- package/dist/web/launch.js +0 -96
- package/dist/web/server.d.ts +0 -20
- package/dist/web/server.js +0 -233
- package/dist/web/types.d.ts +0 -65
- package/dist/web/types.js +0 -2
- package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
- package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
- package/dist/webapp/favicon.ico +0 -0
- package/dist/webapp/index.html +0 -15
package/dist/contract-suite.d.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Shared contract suite for harness plugin tests.
|
|
3
3
|
*
|
|
4
4
|
* Every harness plugin translates the same core decisions (session gates,
|
|
5
|
-
* `gateToolCall`,
|
|
5
|
+
* `gateToolCall`, the not-connected short-circuit) onto its own native signals (exit codes,
|
|
6
6
|
* `{ decision: "block" }`, `{ block: true }`, thrown errors, …). The
|
|
7
7
|
* *decision semantics* are core-owned and tested once in `packages/core`;
|
|
8
8
|
* what varies per harness is only the translation. This suite re-runs the
|
|
9
9
|
* canonical scenario tables through a harness's real entry point via a
|
|
10
10
|
* small adapter, so a plugin cannot ship with a divergent understanding of
|
|
11
|
-
* observe / enforce / fail-open /
|
|
11
|
+
* observe / enforce / fail-open / not-connected — without each plugin
|
|
12
12
|
* re-transcribing the tables.
|
|
13
13
|
*
|
|
14
14
|
* A harness test file calls:
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* and then adds only its genuinely harness-specific tests: export shape,
|
|
30
30
|
* event-name dispatch, block-signal details, input parsing (MCP names,
|
|
31
|
-
* `subagent_type`), unique
|
|
31
|
+
* `subagent_type`), unique activity enrichment, and the install CLI.
|
|
32
32
|
*/
|
|
33
33
|
import { OryAgentClient } from "./client.js";
|
|
34
34
|
import type { ensureUserAuthenticated } from "./user-login.js";
|
|
@@ -69,8 +69,6 @@ export interface HarnessContractAdapter {
|
|
|
69
69
|
sessionCanBlock?: boolean;
|
|
70
70
|
/** Whether the tool gate can hard-block. Defaults to true. */
|
|
71
71
|
toolCanBlock?: boolean;
|
|
72
|
-
/** Set false when the plugin has no legacy OAuth2-token session path. */
|
|
73
|
-
legacyOAuth2?: boolean;
|
|
74
72
|
/** Drive the session-start phase through the plugin's real entry point. */
|
|
75
73
|
sessionStart(ctx: ContractContext): Promise<ContractOutcome>;
|
|
76
74
|
/** Drive the pre-tool gate for `tool` through the plugin's real entry point. */
|
package/dist/contract-suite.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Shared contract suite for harness plugin tests.
|
|
4
4
|
*
|
|
5
5
|
* Every harness plugin translates the same core decisions (session gates,
|
|
6
|
-
* `gateToolCall`,
|
|
6
|
+
* `gateToolCall`, the not-connected short-circuit) onto its own native signals (exit codes,
|
|
7
7
|
* `{ decision: "block" }`, `{ block: true }`, thrown errors, …). The
|
|
8
8
|
* *decision semantics* are core-owned and tested once in `packages/core`;
|
|
9
9
|
* what varies per harness is only the translation. This suite re-runs the
|
|
10
10
|
* canonical scenario tables through a harness's real entry point via a
|
|
11
11
|
* small adapter, so a plugin cannot ship with a divergent understanding of
|
|
12
|
-
* observe / enforce / fail-open /
|
|
12
|
+
* observe / enforce / fail-open / not-connected — without each plugin
|
|
13
13
|
* re-transcribing the tables.
|
|
14
14
|
*
|
|
15
15
|
* A harness test file calls:
|
|
@@ -29,12 +29,11 @@
|
|
|
29
29
|
*
|
|
30
30
|
* and then adds only its genuinely harness-specific tests: export shape,
|
|
31
31
|
* event-name dispatch, block-signal details, input parsing (MCP names,
|
|
32
|
-
* `subagent_type`), unique
|
|
32
|
+
* `subagent_type`), unique activity enrichment, and the install CLI.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.runHarnessContractSuite = runHarnessContractSuite;
|
|
36
36
|
const vitest_1 = require("vitest");
|
|
37
|
-
const config_js_1 = require("./config.js");
|
|
38
37
|
const testing_js_1 = require("./testing.js");
|
|
39
38
|
const DEFAULT_DECISION = {
|
|
40
39
|
proceed: true,
|
|
@@ -59,16 +58,13 @@ function makeGates(userDecision = DEFAULT_DECISION) {
|
|
|
59
58
|
}
|
|
60
59
|
const ENV_KEYS = [
|
|
61
60
|
"ORY_PROJECT_URL",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"ORY_OAUTH2_TOKEN",
|
|
65
|
-
"ORY_USER_SESSION_TOKEN",
|
|
61
|
+
"ORY_SDK_URL",
|
|
62
|
+
"ORY_OAUTH2_CLIENT_ID",
|
|
66
63
|
"ORY_USER_OAUTH2_TOKEN",
|
|
67
|
-
"ORY_USER_LOGIN",
|
|
68
64
|
"ORY_USER_SUBJECT_ID",
|
|
69
65
|
"ORY_USER_SUBJECT_NAMESPACE",
|
|
70
66
|
"ORY_AGENT_SUBJECT_ID",
|
|
71
|
-
"
|
|
67
|
+
"ORY_PERMISSION_MODE_TTL_MS",
|
|
72
68
|
"ORY_PERMISSION_NAMESPACE",
|
|
73
69
|
"ORY_INTERACTIVE_TOOLS",
|
|
74
70
|
];
|
|
@@ -88,7 +84,16 @@ function runHarnessContractSuite(adapter) {
|
|
|
88
84
|
delete process.env[key];
|
|
89
85
|
}
|
|
90
86
|
restoreConfigDir = (0, testing_js_1.useTempConfigDir)();
|
|
87
|
+
// Agent Security runs exactly when both values are present. The contract
|
|
88
|
+
// matrix exercises the *connected* posture, so set both; the dedicated
|
|
89
|
+
// not-connected test clears them again.
|
|
91
90
|
process.env.ORY_PROJECT_URL = "https://test.projects.oryapis.com";
|
|
91
|
+
process.env.ORY_OAUTH2_CLIENT_ID = "test-login-client";
|
|
92
|
+
// Disable the mode cache TTL so each row re-reads the stubbed mode.
|
|
93
|
+
process.env.ORY_PERMISSION_MODE_TTL_MS = "0";
|
|
94
|
+
// Default the stubbed server-read mode to enforce (the matrix rows set it
|
|
95
|
+
// per-case via setTestPermissionMode).
|
|
96
|
+
(0, testing_js_1.setTestPermissionMode)("enforce");
|
|
92
97
|
client = (0, testing_js_1.createMockClient)({ harness: adapter.harness });
|
|
93
98
|
});
|
|
94
99
|
(0, vitest_1.afterEach)(() => {
|
|
@@ -103,21 +108,23 @@ function runHarnessContractSuite(adapter) {
|
|
|
103
108
|
const ctx = (gates = makeGates()) => ({ client, gates });
|
|
104
109
|
// ── Session start never blocks on auth/infra failures ─────────
|
|
105
110
|
(0, vitest_1.describe)("session start is fail-open", () => {
|
|
111
|
+
// The stubs below back APIs the session-start path no longer calls
|
|
112
|
+
// (`verifySession` / `introspectToken`). They stay because the invariant
|
|
113
|
+
// under test is that session start proceeds *regardless* of what those
|
|
114
|
+
// would have said — a plugin that reintroduced a verification
|
|
115
|
+
// fall-through would start failing these rows.
|
|
106
116
|
vitest_1.it.each([
|
|
107
|
-
{ name: "
|
|
108
|
-
{ name: "inactive session", env: "
|
|
109
|
-
{ name: "session network error", env: "
|
|
110
|
-
{ name: "MFA required", env: "
|
|
111
|
-
{ name: "
|
|
112
|
-
{ name: "inactive OAuth2
|
|
117
|
+
{ name: "a pre-supplied token", env: "token", stub: testing_js_1.stubSessionSuccess },
|
|
118
|
+
{ name: "an inactive session upstream", env: "token", stub: testing_js_1.stubSessionInactive },
|
|
119
|
+
{ name: "a session network error", env: "token", stub: testing_js_1.stubSessionNetworkError },
|
|
120
|
+
{ name: "MFA required upstream", env: "token", stub: testing_js_1.stubSessionMfaRequired },
|
|
121
|
+
{ name: "an active OAuth2 introspection", env: "token", stub: testing_js_1.stubOAuth2Success },
|
|
122
|
+
{ name: "an inactive OAuth2 introspection", env: "token", stub: testing_js_1.stubOAuth2Inactive },
|
|
113
123
|
{ name: "unconfigured (no project URL)", env: "none", stub: undefined },
|
|
114
|
-
])("proceeds with $name", async ({ env, stub
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.env.ORY_SESSION_TOKEN = "token-under-test";
|
|
119
|
-
if (env === "oauth2")
|
|
120
|
-
process.env.ORY_OAUTH2_TOKEN = "token-under-test";
|
|
124
|
+
])("proceeds with $name", async ({ env, stub }) => {
|
|
125
|
+
// `ORY_USER_OAUTH2_TOKEN` is the one accepted env credential.
|
|
126
|
+
if (env === "token")
|
|
127
|
+
process.env.ORY_USER_OAUTH2_TOKEN = "token-under-test";
|
|
121
128
|
if (env === "none")
|
|
122
129
|
delete process.env.ORY_PROJECT_URL;
|
|
123
130
|
stub?.(client);
|
|
@@ -138,6 +145,7 @@ function runHarnessContractSuite(adapter) {
|
|
|
138
145
|
const gates = makeGates(declined);
|
|
139
146
|
const outcome = await adapter.sessionStart(ctx(gates));
|
|
140
147
|
(0, vitest_1.expect)(outcome.blocked).toBe(false);
|
|
148
|
+
(0, vitest_1.expect)((0, testing_js_1.getActivityEvents)(client, "session.start")).toHaveLength(1);
|
|
141
149
|
});
|
|
142
150
|
(0, vitest_1.it)("runs the agent gate even when the user gate declines", async () => {
|
|
143
151
|
const gates = makeGates(declined);
|
|
@@ -147,7 +155,7 @@ function runHarnessContractSuite(adapter) {
|
|
|
147
155
|
(0, vitest_1.it)("never runs a legacy session-token verification path", async () => {
|
|
148
156
|
// The user gate owns authentication; there is no separate legacy
|
|
149
157
|
// verify/introspect fall-through any longer.
|
|
150
|
-
process.env.
|
|
158
|
+
process.env.ORY_USER_OAUTH2_TOKEN = "should-not-be-verified";
|
|
151
159
|
const verify = (0, testing_js_1.stubSessionSuccess)(client);
|
|
152
160
|
const outcome = await adapter.sessionStart(ctx());
|
|
153
161
|
(0, vitest_1.expect)(outcome.blocked).toBe(false);
|
|
@@ -160,8 +168,8 @@ function runHarnessContractSuite(adapter) {
|
|
|
160
168
|
{ check: "allowed", mode: "enforce", blocked: false, invoke: true, block: false },
|
|
161
169
|
{ check: "denied", mode: "enforce", blocked: toolCanBlock, invoke: false, block: true },
|
|
162
170
|
{ check: "denied", mode: "observe", blocked: false, invoke: true, block: true },
|
|
163
|
-
{ check: "network_error", mode: "enforce", blocked: false, invoke:
|
|
164
|
-
{ check: "rate_limited", mode: "enforce", blocked: false, invoke:
|
|
171
|
+
{ check: "network_error", mode: "enforce", blocked: false, invoke: true, block: false },
|
|
172
|
+
{ check: "rate_limited", mode: "enforce", blocked: false, invoke: true, block: false },
|
|
165
173
|
])("check=$check × $mode → blocked=$blocked", async ({ check, mode, blocked, invoke, block }) => {
|
|
166
174
|
const stubs = {
|
|
167
175
|
allowed: testing_js_1.stubPermissionAllowed,
|
|
@@ -170,54 +178,74 @@ function runHarnessContractSuite(adapter) {
|
|
|
170
178
|
rate_limited: testing_js_1.stubPermissionRateLimited,
|
|
171
179
|
};
|
|
172
180
|
stubs[check](client);
|
|
173
|
-
|
|
181
|
+
(0, testing_js_1.setTestPermissionMode)(mode);
|
|
174
182
|
const outcome = await adapter.toolBefore(ctx(), adapter.tool);
|
|
175
183
|
(0, vitest_1.expect)(outcome.blocked).toBe(blocked);
|
|
176
184
|
if (blocked)
|
|
177
185
|
(0, vitest_1.expect)(outcome.reason).toBeTruthy();
|
|
178
|
-
const
|
|
179
|
-
const
|
|
186
|
+
const invokeEvents = (0, testing_js_1.getActivityEvents)(client, "tool.invoke");
|
|
187
|
+
const blockEvents = (0, testing_js_1.getActivityEvents)(client, "tool.block");
|
|
180
188
|
if (invoke) {
|
|
181
|
-
(0, vitest_1.expect)(
|
|
182
|
-
(0, vitest_1.expect)(
|
|
189
|
+
(0, vitest_1.expect)(invokeEvents.length).toBeGreaterThanOrEqual(1);
|
|
190
|
+
(0, vitest_1.expect)(invokeEvents[0].attributes?.toolName).toBe(adapter.tool);
|
|
183
191
|
}
|
|
184
192
|
else {
|
|
185
|
-
(0, vitest_1.expect)(
|
|
193
|
+
(0, vitest_1.expect)(invokeEvents).toHaveLength(0);
|
|
186
194
|
}
|
|
187
195
|
if (block) {
|
|
188
|
-
(0, vitest_1.expect)(
|
|
189
|
-
(0, vitest_1.expect)(
|
|
196
|
+
(0, vitest_1.expect)(blockEvents.length).toBeGreaterThanOrEqual(1);
|
|
197
|
+
(0, vitest_1.expect)(blockEvents[0].attributes?.blocked).toBe(mode === "enforce" && toolCanBlock);
|
|
198
|
+
(0, vitest_1.expect)(blockEvents[0].attributes?.observed).toBe(mode === "observe" ? true : undefined);
|
|
190
199
|
}
|
|
191
200
|
else {
|
|
192
|
-
(0, vitest_1.expect)(
|
|
201
|
+
(0, vitest_1.expect)(blockEvents).toHaveLength(0);
|
|
193
202
|
}
|
|
194
|
-
// Observe-mode denies also emit the audit
|
|
195
|
-
const
|
|
196
|
-
(0, vitest_1.expect)(
|
|
203
|
+
// Observe-mode denies also emit the audit event; enforce must not.
|
|
204
|
+
const observeEvents = (0, testing_js_1.getActivityEvents)(client, "permission.observe_deny");
|
|
205
|
+
(0, vitest_1.expect)(observeEvents).toHaveLength(check === "denied" && mode === "observe" ? 1 : 0);
|
|
197
206
|
});
|
|
198
|
-
(0, vitest_1.it)("
|
|
199
|
-
|
|
207
|
+
(0, vitest_1.it)("records tool.invoke when an interactive tool proceeds", async () => {
|
|
208
|
+
process.env.ORY_INTERACTIVE_TOOLS = adapter.tool;
|
|
209
|
+
const outcome = await adapter.toolBefore(ctx(), adapter.tool);
|
|
210
|
+
(0, vitest_1.expect)(outcome.blocked).toBe(false);
|
|
211
|
+
(0, vitest_1.expect)((0, testing_js_1.getActivityEvents)(client, "tool.invoke")).toHaveLength(1);
|
|
212
|
+
});
|
|
213
|
+
// A project URL is the required address. The public login client has a
|
|
214
|
+
// reserved default, so omitting only its override remains connected.
|
|
215
|
+
vitest_1.it.each([
|
|
216
|
+
["no project URL and no client id", { url: false, clientId: false }],
|
|
217
|
+
["a client id but no project URL", { url: false, clientId: true }],
|
|
218
|
+
])("skips the permission check entirely with %s", async (_label, { url, clientId }) => {
|
|
219
|
+
if (!url)
|
|
220
|
+
delete process.env.ORY_PROJECT_URL;
|
|
221
|
+
if (!clientId)
|
|
222
|
+
delete process.env.ORY_OAUTH2_CLIENT_ID;
|
|
200
223
|
const checkSpy = vitest_1.vi.spyOn(client, "checkPermission");
|
|
201
|
-
|
|
224
|
+
(0, testing_js_1.setTestPermissionMode)("enforce");
|
|
202
225
|
const outcome = await adapter.toolBefore(ctx(), adapter.tool);
|
|
226
|
+
// Never blocks: with nothing to check against, the tool passes through.
|
|
203
227
|
(0, vitest_1.expect)(outcome.blocked).toBe(false);
|
|
204
228
|
(0, vitest_1.expect)(checkSpy).not.toHaveBeenCalled();
|
|
205
229
|
});
|
|
206
|
-
(0, vitest_1.it)("addresses the check to ORY_USER_SUBJECT_ID when set", async () => {
|
|
230
|
+
(0, vitest_1.it)("addresses the check to ORY_USER_SUBJECT_ID when set (as a User SubjectSet)", async () => {
|
|
207
231
|
process.env.ORY_USER_SUBJECT_ID = "user:custom-override";
|
|
208
232
|
(0, testing_js_1.stubPermissionAllowed)(client);
|
|
209
233
|
await adapter.toolBefore(ctx(), adapter.tool);
|
|
210
|
-
const [
|
|
211
|
-
|
|
234
|
+
const [activity] = (0, testing_js_1.getActivityEvents)(client, "tool.invoke");
|
|
235
|
+
// Principals are addressed as SubjectSets under the default `User`
|
|
236
|
+
// namespace; activity carries the SubjectSet, not a direct subjectId.
|
|
237
|
+
(0, vitest_1.expect)(activity.attributes?.subjectSet).toBeUndefined();
|
|
238
|
+
(0, vitest_1.expect)(activity.attributes?.subjectSetHash).toMatch(/^[a-f0-9]{16}$/);
|
|
239
|
+
(0, vitest_1.expect)(activity.attributes?.subjectId).toBeUndefined();
|
|
212
240
|
});
|
|
213
241
|
});
|
|
214
242
|
// ── Post-tool + passthrough ────────────────────────────────────
|
|
215
243
|
if (adapter.toolAfter) {
|
|
216
244
|
(0, vitest_1.it)("records tool.complete after execution", async () => {
|
|
217
245
|
await adapter.toolAfter(ctx(), adapter.tool);
|
|
218
|
-
const
|
|
219
|
-
(0, vitest_1.expect)(
|
|
220
|
-
(0, vitest_1.expect)(
|
|
246
|
+
const events = (0, testing_js_1.getActivityEvents)(client, "tool.complete");
|
|
247
|
+
(0, vitest_1.expect)(events.length).toBeGreaterThanOrEqual(1);
|
|
248
|
+
(0, vitest_1.expect)(events[0].attributes?.toolName).toBe(adapter.tool);
|
|
221
249
|
});
|
|
222
250
|
}
|
|
223
251
|
if (adapter.unknownEvent) {
|
package/dist/denial.d.ts
CHANGED
|
@@ -15,11 +15,35 @@ export interface DenialContext {
|
|
|
15
15
|
subjectId: string;
|
|
16
16
|
namespace?: string;
|
|
17
17
|
mcp?: McpToolIdentifier;
|
|
18
|
+
/**
|
|
19
|
+
* Shell decomposition (issue #76): the denied command word (`curl`) inside a
|
|
20
|
+
* `Bash` call. When set, the message names the word and the
|
|
21
|
+
* `ShellTool:<word>#use` permission instead of the tool.
|
|
22
|
+
*/
|
|
23
|
+
shellWord?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Every denied command word when more than one was blocked in the same shell
|
|
26
|
+
* call. The message lists them all so the user can grant them in one pass;
|
|
27
|
+
* {@link shellWord} remains the primary (first) word for back-compat.
|
|
28
|
+
*/
|
|
29
|
+
shellDeniedWords?: string[];
|
|
30
|
+
/** Set when a shell command was blocked because it could not be safely parsed. */
|
|
31
|
+
shellTooComplex?: boolean;
|
|
32
|
+
/** Namespace for shell-command sub-tools (default `ShellTool`). */
|
|
33
|
+
shellNamespace?: string;
|
|
34
|
+
/**
|
|
35
|
+
* True when the deny was an *explicit block* (a `blocked` relation matched
|
|
36
|
+
* the subject) rather than a missing grant. Changes the wording from "not
|
|
37
|
+
* authorized" to "explicitly blocked" and points the user at removing the
|
|
38
|
+
* block rather than granting a permission. Sourced from a decision's
|
|
39
|
+
* `activityAttributes.blockReason === "explicit_block"`.
|
|
40
|
+
*/
|
|
41
|
+
blocked?: boolean;
|
|
18
42
|
}
|
|
19
43
|
/**
|
|
20
|
-
*
|
|
44
|
+
* Activity attributes that mark a denial as a security alert.
|
|
21
45
|
*
|
|
22
|
-
* Spread these onto the existing `tool.block`
|
|
46
|
+
* Spread these onto the existing `tool.block` activity attributes whenever an
|
|
23
47
|
* actual deny comes back from Ory. The shape is stable so downstream
|
|
24
48
|
* alerting rules can hook on `attributes.alert === true` and filter by
|
|
25
49
|
* `attributes.severity` or `attributes.alertKind` without string matching.
|
|
@@ -40,7 +64,7 @@ export declare function alertAttributes(blocked: boolean): AlertAttributes;
|
|
|
40
64
|
/**
|
|
41
65
|
* Human-facing denial reason for an alert that the harness cannot enforce
|
|
42
66
|
* (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
|
|
43
|
-
* banner and notes that the
|
|
67
|
+
* banner and notes that the activity event is the alert signal, so the agent
|
|
44
68
|
* — and any human reading transcripts — can't miss that a security event
|
|
45
69
|
* was emitted even though the tool was allowed to proceed.
|
|
46
70
|
*/
|
|
@@ -50,6 +74,15 @@ export declare function formatAlertMessage(ctx: DenialContext): string;
|
|
|
50
74
|
* systemMessage, etc.) when the denial cannot be enforced.
|
|
51
75
|
*/
|
|
52
76
|
export declare function formatAlertSummary(ctx: DenialContext): string;
|
|
77
|
+
/**
|
|
78
|
+
* Extract the shell-decomposition denial fields (issue #76) from a permission
|
|
79
|
+
* decision's activity attributes, for spreading into a {@link DenialContext}. Reads
|
|
80
|
+
* defensively (the outcome union includes non-shell members) so harness
|
|
81
|
+
* handlers can write `...shellDenialFields(decision)` without narrowing.
|
|
82
|
+
*/
|
|
83
|
+
export declare function shellDenialFields(decision: {
|
|
84
|
+
activityAttributes?: unknown;
|
|
85
|
+
}): Pick<DenialContext, "shellWord" | "shellDeniedWords" | "shellTooComplex" | "shellNamespace" | "blocked">;
|
|
53
86
|
/**
|
|
54
87
|
* Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
|
|
55
88
|
* reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
|
package/dist/denial.js
CHANGED
|
@@ -15,9 +15,21 @@ exports.OryDenialError = void 0;
|
|
|
15
15
|
exports.alertAttributes = alertAttributes;
|
|
16
16
|
exports.formatAlertMessage = formatAlertMessage;
|
|
17
17
|
exports.formatAlertSummary = formatAlertSummary;
|
|
18
|
+
exports.shellDenialFields = shellDenialFields;
|
|
18
19
|
exports.formatDenialMessage = formatDenialMessage;
|
|
19
20
|
exports.formatDenialSummary = formatDenialSummary;
|
|
20
|
-
const
|
|
21
|
+
const tool_catalog_js_1 = require("./tool-catalog.js");
|
|
22
|
+
/**
|
|
23
|
+
* Where a user is sent when a tool is denied.
|
|
24
|
+
*
|
|
25
|
+
* Names the **Ory Console** rather than "your Ory Keto administrator": since
|
|
26
|
+
* #180 the plugin is a read-only Keto client and grants, blocks, and the
|
|
27
|
+
* permission model are all provisioned in the Console by someone with project
|
|
28
|
+
* access. There is no "Keto administrator" role to contact, and "Keto" is
|
|
29
|
+
* internal vocabulary that should never reach an end user (#222).
|
|
30
|
+
*/
|
|
31
|
+
const ADMIN_HINT = "Ask someone with access to your Ory project to grant this permission in the Ory Console (Agent Security).";
|
|
32
|
+
const BLOCK_HINT = "This subject is explicitly blocked from this tool. Ask someone with access to your Ory project to remove the block in the Ory Console (Agent Security).";
|
|
21
33
|
function alertAttributes(blocked) {
|
|
22
34
|
return {
|
|
23
35
|
alert: true,
|
|
@@ -30,12 +42,12 @@ const ALERT_PREFIX = "ORY SECURITY ALERT TRIGGERED";
|
|
|
30
42
|
/**
|
|
31
43
|
* Human-facing denial reason for an alert that the harness cannot enforce
|
|
32
44
|
* (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
|
|
33
|
-
* banner and notes that the
|
|
45
|
+
* banner and notes that the activity event is the alert signal, so the agent
|
|
34
46
|
* — and any human reading transcripts — can't miss that a security event
|
|
35
47
|
* was emitted even though the tool was allowed to proceed.
|
|
36
48
|
*/
|
|
37
49
|
function formatAlertMessage(ctx) {
|
|
38
|
-
return `[${ALERT_PREFIX}] ${formatDenialMessage(ctx)} A security
|
|
50
|
+
return `[${ALERT_PREFIX}] ${formatDenialMessage(ctx)} A security activity event was recorded for this denied tool call.`;
|
|
39
51
|
}
|
|
40
52
|
/**
|
|
41
53
|
* Short alert summary suitable for compact UI fields (Gemini's
|
|
@@ -44,38 +56,95 @@ function formatAlertMessage(ctx) {
|
|
|
44
56
|
function formatAlertSummary(ctx) {
|
|
45
57
|
return `[${ALERT_PREFIX}] ${formatDenialSummary(ctx)}`;
|
|
46
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract the shell-decomposition denial fields (issue #76) from a permission
|
|
61
|
+
* decision's activity attributes, for spreading into a {@link DenialContext}. Reads
|
|
62
|
+
* defensively (the outcome union includes non-shell members) so harness
|
|
63
|
+
* handlers can write `...shellDenialFields(decision)` without narrowing.
|
|
64
|
+
*/
|
|
65
|
+
function shellDenialFields(decision) {
|
|
66
|
+
const sa = (decision.activityAttributes ?? {});
|
|
67
|
+
return {
|
|
68
|
+
shellWord: sa.shellWord,
|
|
69
|
+
shellDeniedWords: sa.shellDeniedWords,
|
|
70
|
+
shellTooComplex: sa.shellTooComplex,
|
|
71
|
+
shellNamespace: (0, tool_catalog_js_1.resolveShellCommandNamespace)(),
|
|
72
|
+
blocked: sa.blockReason === "explicit_block",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
47
75
|
/**
|
|
48
76
|
* Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
|
|
49
77
|
* reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
|
|
50
78
|
* thrown error message).
|
|
51
79
|
*/
|
|
52
80
|
function formatDenialMessage(ctx) {
|
|
81
|
+
if (ctx.shellTooComplex) {
|
|
82
|
+
return (`Ory: shell command blocked — it could not be safely parsed and is ` +
|
|
83
|
+
`denied in enforce mode. Subject "${ctx.subjectId}". ` +
|
|
84
|
+
`Simplify the command or split it into individually authorized steps.`);
|
|
85
|
+
}
|
|
86
|
+
if (ctx.shellWord) {
|
|
87
|
+
const ns = ctx.shellNamespace ?? "ShellTool";
|
|
88
|
+
const shellHint = ctx.blocked ? BLOCK_HINT : ADMIN_HINT;
|
|
89
|
+
const shellVerb = ctx.blocked
|
|
90
|
+
? "is explicitly blocked from running"
|
|
91
|
+
: "is not authorized to run";
|
|
92
|
+
const denied = ctx.shellDeniedWords && ctx.shellDeniedWords.length > 0
|
|
93
|
+
? ctx.shellDeniedWords
|
|
94
|
+
: [ctx.shellWord];
|
|
95
|
+
if (denied.length > 1) {
|
|
96
|
+
const list = denied.map((w) => `"${w}"`).join(", ");
|
|
97
|
+
const perms = denied.map((w) => `"${ns}:${w}#use"`).join(", ");
|
|
98
|
+
return (`Ory: permission denied for shell commands ${list}. ` +
|
|
99
|
+
`Subject "${ctx.subjectId}" ${shellVerb} them ` +
|
|
100
|
+
`(permissions ${perms}). ` +
|
|
101
|
+
shellHint);
|
|
102
|
+
}
|
|
103
|
+
return (`Ory: permission denied for shell command "${ctx.shellWord}". ` +
|
|
104
|
+
`Subject "${ctx.subjectId}" ${shellVerb} "${ctx.shellWord}" ` +
|
|
105
|
+
`(permission "${ns}:${ctx.shellWord}#use"). ` +
|
|
106
|
+
shellHint);
|
|
107
|
+
}
|
|
108
|
+
const hint = ctx.blocked ? BLOCK_HINT : ADMIN_HINT;
|
|
53
109
|
if (ctx.mcp) {
|
|
54
110
|
const tool = ctx.mcp.toolName
|
|
55
111
|
? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
|
|
56
112
|
: ctx.mcp.serverName;
|
|
113
|
+
const verb = ctx.blocked ? "is explicitly blocked from" : "is not authorized to use";
|
|
57
114
|
return (`Ory: permission denied for MCP server "${ctx.mcp.serverName}". ` +
|
|
58
|
-
`Subject "${ctx.subjectId}"
|
|
59
|
-
|
|
115
|
+
`Subject "${ctx.subjectId}" ${verb} "${tool}". ` +
|
|
116
|
+
hint);
|
|
60
117
|
}
|
|
61
|
-
const ns = ctx.namespace ?? "
|
|
118
|
+
const ns = ctx.namespace ?? "AgentTool";
|
|
119
|
+
const verb = ctx.blocked ? "is explicitly blocked from invoking" : "is not authorized to invoke";
|
|
62
120
|
return (`Ory: permission denied for "${ctx.tool}". ` +
|
|
63
|
-
`Subject "${ctx.subjectId}"
|
|
121
|
+
`Subject "${ctx.subjectId}" ${verb} "${ctx.tool}" ` +
|
|
64
122
|
`(namespace "${ns}"). ` +
|
|
65
|
-
|
|
123
|
+
hint);
|
|
66
124
|
}
|
|
67
125
|
/**
|
|
68
126
|
* Short denial summary suitable for compact UI fields (e.g. Gemini's
|
|
69
127
|
* systemMessage which appears inline in the agent's TUI).
|
|
70
128
|
*/
|
|
71
129
|
function formatDenialSummary(ctx) {
|
|
130
|
+
if (ctx.shellTooComplex) {
|
|
131
|
+
return `Ory: shell command blocked (could not be safely parsed). ${ADMIN_HINT}`;
|
|
132
|
+
}
|
|
133
|
+
if (ctx.shellWord) {
|
|
134
|
+
const denied = ctx.shellDeniedWords && ctx.shellDeniedWords.length > 1
|
|
135
|
+
? ctx.shellDeniedWords.map((w) => `"${w}"`).join(", ")
|
|
136
|
+
: `"${ctx.shellWord}"`;
|
|
137
|
+
const noun = ctx.shellDeniedWords && ctx.shellDeniedWords.length > 1 ? "commands" : "command";
|
|
138
|
+
return `Ory: permission denied for shell ${noun} ${denied}. ${ctx.blocked ? BLOCK_HINT : ADMIN_HINT}`;
|
|
139
|
+
}
|
|
140
|
+
const hint = ctx.blocked ? BLOCK_HINT : ADMIN_HINT;
|
|
72
141
|
if (ctx.mcp) {
|
|
73
142
|
const tool = ctx.mcp.toolName
|
|
74
143
|
? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
|
|
75
144
|
: ctx.mcp.serverName;
|
|
76
|
-
return `Ory: permission denied for MCP tool "${tool}". ${
|
|
145
|
+
return `Ory: permission denied for MCP tool "${tool}". ${hint}`;
|
|
77
146
|
}
|
|
78
|
-
return `Ory: permission denied for "${ctx.tool}". ${
|
|
147
|
+
return `Ory: permission denied for "${ctx.tool}". ${hint}`;
|
|
79
148
|
}
|
|
80
149
|
/**
|
|
81
150
|
* Thrown by plugins whose hook contract has no native block signal and
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ActivityEntry, ActivityStatus, DebugLogger } from "./logger.js";
|
|
2
|
+
export declare const SERVER_ACTIVITY_EVENTS: Set<string>;
|
|
3
|
+
export interface ServerActivityEvent {
|
|
4
|
+
event_id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: ActivityStatus;
|
|
7
|
+
occurred_at: string;
|
|
8
|
+
session_id?: string;
|
|
9
|
+
attributes?: Record<string, string | number | boolean | string[]>;
|
|
10
|
+
}
|
|
11
|
+
export interface EventReporterOptions {
|
|
12
|
+
projectUrl: string;
|
|
13
|
+
agentSecurityUrl: string;
|
|
14
|
+
token?: (sessionId?: string, principalKey?: string) => string | undefined;
|
|
15
|
+
request?: (url: string, init: RequestInit, sessionId?: string, principalKey?: string) => Promise<Response> | undefined;
|
|
16
|
+
onUnauthorized?: (sessionId: string | undefined, principalKey: string | undefined, rejectedToken: string) => void;
|
|
17
|
+
logger?: DebugLogger;
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
maxQueueSize?: number;
|
|
20
|
+
requestTimeoutMs?: number;
|
|
21
|
+
/** Durable per-event storage. Omitted reporters remain memory-only. */
|
|
22
|
+
outboxDir?: string;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
eventId?: () => string;
|
|
25
|
+
retryBaseMs?: number;
|
|
26
|
+
retryMaxMs?: number;
|
|
27
|
+
random?: () => number;
|
|
28
|
+
}
|
|
29
|
+
export interface PendingEventScope {
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
principalKey?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Best-effort delivery of privacy-safe client activity to Agent Security. */
|
|
34
|
+
export declare class AgentSecurityEventReporter {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly queue;
|
|
37
|
+
private readonly fetcher;
|
|
38
|
+
private readonly maxQueueSize;
|
|
39
|
+
private readonly requestTimeoutMs;
|
|
40
|
+
private readonly now;
|
|
41
|
+
private readonly eventId;
|
|
42
|
+
private delivery;
|
|
43
|
+
private scheduled;
|
|
44
|
+
private activeRequest;
|
|
45
|
+
private flushDeadline;
|
|
46
|
+
private suppressRetry;
|
|
47
|
+
private retryTimer;
|
|
48
|
+
private retryAttempt;
|
|
49
|
+
private readonly retryBaseMs;
|
|
50
|
+
private readonly retryMaxMs;
|
|
51
|
+
private readonly random;
|
|
52
|
+
constructor(options: EventReporterOptions);
|
|
53
|
+
get pending(): number;
|
|
54
|
+
get pendingScopes(): ReadonlyArray<PendingEventScope>;
|
|
55
|
+
enqueue(entry: ActivityEntry, principalKey?: string): void;
|
|
56
|
+
/** Attempt all currently deliverable batches, bounded by the caller's deadline. */
|
|
57
|
+
flush(timeoutMs?: number): Promise<void>;
|
|
58
|
+
private schedule;
|
|
59
|
+
private startDelivery;
|
|
60
|
+
private scheduleRetry;
|
|
61
|
+
private drain;
|
|
62
|
+
private deliver;
|
|
63
|
+
private request;
|
|
64
|
+
private persist;
|
|
65
|
+
private syncOutbox;
|
|
66
|
+
private acquireOutboxLock;
|
|
67
|
+
private cleanRenamedLocks;
|
|
68
|
+
private enforceBounds;
|
|
69
|
+
private removeFile;
|
|
70
|
+
private prune;
|
|
71
|
+
private diagnostic;
|
|
72
|
+
}
|
|
73
|
+
export declare function projectActivityEntry(entry: ActivityEntry, createEventId?: () => string): ServerActivityEvent | undefined;
|
|
74
|
+
/** Stable, non-identifying directory name that prevents cross-project replay. */
|
|
75
|
+
export declare function eventOutboxProjectScope(projectUrl: string): string;
|
|
76
|
+
/** Stable local selector for a bearer without persisting its identifying subject. */
|
|
77
|
+
export declare function eventPrincipalKey(subject: string): string;
|