@ory/argus 0.1.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 +134 -0
- package/assets/commands/local-down.md +19 -0
- package/assets/commands/local-up.md +27 -0
- package/assets/skills/auth-setup/SKILL.md +279 -0
- package/assets/skills/local-dev/SKILL.md +206 -0
- package/assets/skills/login-flow/SKILL.md +383 -0
- package/assets/skills/social-login/SKILL.md +312 -0
- package/dist/agent-auth.d.ts +204 -0
- package/dist/agent-auth.js +553 -0
- package/dist/auth-gate.d.ts +71 -0
- package/dist/auth-gate.js +308 -0
- package/dist/auth-store.d.ts +75 -0
- package/dist/auth-store.js +261 -0
- package/dist/auth.d.ts +93 -0
- package/dist/auth.js +323 -0
- package/dist/cli.d.ts +73 -0
- package/dist/cli.js +484 -0
- package/dist/client.d.ts +158 -0
- package/dist/client.js +679 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.js +344 -0
- package/dist/denial.d.ts +79 -0
- package/dist/denial.js +103 -0
- package/dist/dev.d.ts +95 -0
- package/dist/dev.js +514 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +137 -0
- package/dist/local/cli.d.ts +12 -0
- package/dist/local/cli.js +95 -0
- package/dist/local/configs.d.ts +89 -0
- package/dist/local/configs.js +634 -0
- package/dist/local/health.d.ts +32 -0
- package/dist/local/health.js +65 -0
- package/dist/local/index.d.ts +6 -0
- package/dist/local/index.js +38 -0
- package/dist/local/jaeger-main.d.ts +13 -0
- package/dist/local/jaeger-main.js +85 -0
- package/dist/local/jaeger.d.ts +50 -0
- package/dist/local/jaeger.js +162 -0
- package/dist/local/main.d.ts +7 -0
- package/dist/local/main.js +14 -0
- package/dist/local/manager.d.ts +45 -0
- package/dist/local/manager.js +676 -0
- package/dist/local/seed.d.ts +71 -0
- package/dist/local/seed.js +237 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.js +139 -0
- package/dist/mcp.d.ts +76 -0
- package/dist/mcp.js +122 -0
- package/dist/otel/exporter.d.ts +17 -0
- package/dist/otel/exporter.js +12 -0
- package/dist/otel/index.d.ts +2 -0
- package/dist/otel/index.js +8 -0
- package/dist/otel/otlp-http.d.ts +116 -0
- package/dist/otel/otlp-http.js +322 -0
- package/dist/registry/cli.d.ts +12 -0
- package/dist/registry/cli.js +76 -0
- package/dist/registry/config.d.ts +23 -0
- package/dist/registry/config.js +80 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +21 -0
- package/dist/registry/main.d.ts +7 -0
- package/dist/registry/main.js +14 -0
- package/dist/registry/manager.d.ts +38 -0
- package/dist/registry/manager.js +674 -0
- package/dist/setup.d.ts +118 -0
- package/dist/setup.js +398 -0
- package/dist/skills.d.ts +78 -0
- package/dist/skills.js +264 -0
- package/dist/subject.d.ts +43 -0
- package/dist/subject.js +55 -0
- package/dist/tool-metadata.d.ts +41 -0
- package/dist/tool-metadata.js +127 -0
- package/dist/tracer.d.ts +172 -0
- package/dist/tracer.js +452 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +3 -0
- package/dist/watch-sandbox.d.ts +9 -0
- package/dist/watch-sandbox.js +81 -0
- package/package.json +79 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent configuration for Ory agent plugins.
|
|
3
|
+
*
|
|
4
|
+
* Config is stored at ~/.config/ory-agent-plugins/config.json and shared
|
|
5
|
+
* across all harness plugins. Environment variables always take precedence
|
|
6
|
+
* over values in the config file.
|
|
7
|
+
*/
|
|
8
|
+
export interface OryOAuth2Tokens {
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken?: string;
|
|
11
|
+
/** Unix epoch seconds at which the access token expires. */
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
/** Subject (sub claim) the token was issued for. */
|
|
14
|
+
subject?: string;
|
|
15
|
+
/** Client ID the token was issued under. */
|
|
16
|
+
clientId?: string;
|
|
17
|
+
/** Optional id_token (OpenID Connect). */
|
|
18
|
+
idToken?: string;
|
|
19
|
+
/** Scope string returned by the token endpoint. */
|
|
20
|
+
scope?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Credentials for the human user principal (interactive login). */
|
|
23
|
+
export interface OryUserCredentials {
|
|
24
|
+
/** Persisted OAuth2 tokens from a browser-based PKCE login. */
|
|
25
|
+
oauth2?: OryOAuth2Tokens;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Credentials returned by an OAuth2 Dynamic Client Registration flow
|
|
29
|
+
* (RFC 7591). Persisted so that subsequent dev launches and process
|
|
30
|
+
* restarts re-use the same agent identity instead of re-registering on
|
|
31
|
+
* every session.
|
|
32
|
+
*/
|
|
33
|
+
export interface OryAgentDynamicCredentials {
|
|
34
|
+
/** Issued client_id. */
|
|
35
|
+
clientId: string;
|
|
36
|
+
/** Issued client_secret. Omitted for public clients. */
|
|
37
|
+
clientSecret?: string;
|
|
38
|
+
/** Bearer used to manage this registration via RFC 7592. */
|
|
39
|
+
registrationAccessToken?: string;
|
|
40
|
+
/** RFC 7592 management URI for this registration. */
|
|
41
|
+
registrationClientUri?: string;
|
|
42
|
+
/** Unix epoch seconds when this registration was issued. */
|
|
43
|
+
registeredAt: number;
|
|
44
|
+
/** Project URL the registration was issued against. */
|
|
45
|
+
projectUrl: string;
|
|
46
|
+
/** Harness name baked into the client_name (audit aid). */
|
|
47
|
+
harness?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Credentials for the AI agent principal (machine identity). */
|
|
50
|
+
export interface OryAgentCredentialsBlock {
|
|
51
|
+
/** Per-install credentials issued via dynamic client registration. */
|
|
52
|
+
dynamic?: OryAgentDynamicCredentials;
|
|
53
|
+
/**
|
|
54
|
+
* Per-sub-agent dynamic client registrations, keyed by sub-agent type
|
|
55
|
+
* (e.g. `"Explore"`, `"general-purpose"`). Each entry is a distinct
|
|
56
|
+
* OAuth2 client registered via RFC 7591 so the audit trail can
|
|
57
|
+
* attribute actions to a specific sub-agent identity. Reused across
|
|
58
|
+
* sessions for the same `(projectUrl, subAgentType)` pair.
|
|
59
|
+
*/
|
|
60
|
+
subAgents?: Record<string, OryAgentDynamicCredentials>;
|
|
61
|
+
}
|
|
62
|
+
export interface OryPluginConfig {
|
|
63
|
+
projectUrl?: string;
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
/** When true, only audit logging is enabled — no auth or permission checks. */
|
|
66
|
+
auditOnly?: boolean;
|
|
67
|
+
/** Credentials for the human user (interactive PKCE login). */
|
|
68
|
+
user?: OryUserCredentials;
|
|
69
|
+
/** Credentials for the AI agent process (machine identity). */
|
|
70
|
+
agent?: OryAgentCredentialsBlock;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Single OS-agnostic data directory for *all* Ory agent plugin state:
|
|
74
|
+
* the shared config file, persisted DCR credentials, and any harness
|
|
75
|
+
* asset directories. One place per platform — never split across
|
|
76
|
+
* `~/.ory/` and `~/.config/`.
|
|
77
|
+
*
|
|
78
|
+
* - Windows: `%APPDATA%/ory-agent-plugins` (e.g.
|
|
79
|
+
* `C:\Users\<user>\AppData\Roaming\ory-agent-plugins`).
|
|
80
|
+
* - Unix (macOS + Linux): `$XDG_CONFIG_HOME/ory-agent-plugins` when set,
|
|
81
|
+
* otherwise `~/.config/ory-agent-plugins`.
|
|
82
|
+
*
|
|
83
|
+
* `XDG_CONFIG_HOME` is also honored on Windows when set — useful for
|
|
84
|
+
* tests and for users who explicitly opt into XDG layout on Windows.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getDataDir(): string;
|
|
87
|
+
/**
|
|
88
|
+
* Per-harness sub-directory under the shared data dir. Use this for
|
|
89
|
+
* harness-owned asset trees (e.g. the Claude Code plugin's marketplace
|
|
90
|
+
* checkout) so all plugin state lives under one root.
|
|
91
|
+
*/
|
|
92
|
+
export declare function getHarnessDataDir(harness: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Return the path to the config file.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getConfigPath(): string;
|
|
97
|
+
/**
|
|
98
|
+
* Load config from the config file. Returns an empty object if the file
|
|
99
|
+
* does not exist or is invalid.
|
|
100
|
+
*/
|
|
101
|
+
export declare function loadConfig(): OryPluginConfig;
|
|
102
|
+
/**
|
|
103
|
+
* Save config to the config file. Merges with existing values — only
|
|
104
|
+
* provided fields are overwritten. Concurrent processes serialize via a
|
|
105
|
+
* sibling lockfile and the write is atomic (write-temp + rename).
|
|
106
|
+
*
|
|
107
|
+
* To clear a field rather than ignore it, set it to `null` in `update`
|
|
108
|
+
* (e.g. `saveConfig({ user: null })`); `undefined` is treated as
|
|
109
|
+
* "leave alone" for backwards compatibility.
|
|
110
|
+
*/
|
|
111
|
+
export declare function saveConfig(update: Partial<{
|
|
112
|
+
[K in keyof OryPluginConfig]: OryPluginConfig[K] | null;
|
|
113
|
+
}>): void;
|
|
114
|
+
/**
|
|
115
|
+
* Read-modify-write the config under a sibling lockfile. The mutator
|
|
116
|
+
* receives the current on-disk config and must return the new config.
|
|
117
|
+
* Concurrent processes serialize on the lock; the write itself is
|
|
118
|
+
* atomic via write-temp + rename.
|
|
119
|
+
*/
|
|
120
|
+
export declare function mutateConfig(mutator: (current: OryPluginConfig) => OryPluginConfig): void;
|
|
121
|
+
/**
|
|
122
|
+
* Resolve config by checking environment variables first, then the config file.
|
|
123
|
+
* Returns the merged result with the source of each value.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveConfig(): {
|
|
126
|
+
projectUrl?: string;
|
|
127
|
+
apiKey?: string;
|
|
128
|
+
auditOnly: boolean;
|
|
129
|
+
projectUrlSource: "env" | "config" | "none";
|
|
130
|
+
apiKeySource: "env" | "config" | "none";
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Build the "not configured" prompt message for a given harness CLI bin name.
|
|
134
|
+
*/
|
|
135
|
+
export declare function configPromptMessage(binName: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getDataDir = getDataDir;
|
|
37
|
+
exports.getHarnessDataDir = getHarnessDataDir;
|
|
38
|
+
exports.getConfigPath = getConfigPath;
|
|
39
|
+
exports.loadConfig = loadConfig;
|
|
40
|
+
exports.saveConfig = saveConfig;
|
|
41
|
+
exports.mutateConfig = mutateConfig;
|
|
42
|
+
exports.resolveConfig = resolveConfig;
|
|
43
|
+
exports.configPromptMessage = configPromptMessage;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
/**
|
|
47
|
+
* Single OS-agnostic data directory for *all* Ory agent plugin state:
|
|
48
|
+
* the shared config file, persisted DCR credentials, and any harness
|
|
49
|
+
* asset directories. One place per platform — never split across
|
|
50
|
+
* `~/.ory/` and `~/.config/`.
|
|
51
|
+
*
|
|
52
|
+
* - Windows: `%APPDATA%/ory-agent-plugins` (e.g.
|
|
53
|
+
* `C:\Users\<user>\AppData\Roaming\ory-agent-plugins`).
|
|
54
|
+
* - Unix (macOS + Linux): `$XDG_CONFIG_HOME/ory-agent-plugins` when set,
|
|
55
|
+
* otherwise `~/.config/ory-agent-plugins`.
|
|
56
|
+
*
|
|
57
|
+
* `XDG_CONFIG_HOME` is also honored on Windows when set — useful for
|
|
58
|
+
* tests and for users who explicitly opt into XDG layout on Windows.
|
|
59
|
+
*/
|
|
60
|
+
function getDataDir() {
|
|
61
|
+
const xdgOverride = process.env.XDG_CONFIG_HOME?.trim();
|
|
62
|
+
if (xdgOverride)
|
|
63
|
+
return path.join(xdgOverride, "ory-agent-plugins");
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
const appData = process.env.APPDATA?.trim() ||
|
|
66
|
+
path.join(getHomeDir(), "AppData", "Roaming");
|
|
67
|
+
return path.join(appData, "ory-agent-plugins");
|
|
68
|
+
}
|
|
69
|
+
return path.join(getHomeDir(), ".config", "ory-agent-plugins");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Per-harness sub-directory under the shared data dir. Use this for
|
|
73
|
+
* harness-owned asset trees (e.g. the Claude Code plugin's marketplace
|
|
74
|
+
* checkout) so all plugin state lives under one root.
|
|
75
|
+
*/
|
|
76
|
+
function getHarnessDataDir(harness) {
|
|
77
|
+
return path.join(getDataDir(), harness);
|
|
78
|
+
}
|
|
79
|
+
function configDir() {
|
|
80
|
+
return getDataDir();
|
|
81
|
+
}
|
|
82
|
+
function configFile() {
|
|
83
|
+
return path.join(configDir(), "config.json");
|
|
84
|
+
}
|
|
85
|
+
function configLockFile() {
|
|
86
|
+
return configFile() + ".lock";
|
|
87
|
+
}
|
|
88
|
+
/** A lock that is older than this is considered stale and may be broken. */
|
|
89
|
+
const LOCK_STALE_MS = 30_000;
|
|
90
|
+
/** Polling interval while waiting to acquire the lock. */
|
|
91
|
+
const LOCK_POLL_MS = 25;
|
|
92
|
+
/** Maximum total time to wait for the lock before giving up. */
|
|
93
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
94
|
+
function getHomeDir() {
|
|
95
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Return the path to the config file.
|
|
99
|
+
*/
|
|
100
|
+
function getConfigPath() {
|
|
101
|
+
return configFile();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Load config from the config file. Returns an empty object if the file
|
|
105
|
+
* does not exist or is invalid.
|
|
106
|
+
*/
|
|
107
|
+
function loadConfig() {
|
|
108
|
+
try {
|
|
109
|
+
const file = configFile();
|
|
110
|
+
if (!fs.existsSync(file))
|
|
111
|
+
return {};
|
|
112
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
return {
|
|
115
|
+
projectUrl: typeof parsed.projectUrl === "string" ? parsed.projectUrl : undefined,
|
|
116
|
+
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
|
|
117
|
+
auditOnly: parsed.auditOnly === true ? true : undefined,
|
|
118
|
+
user: parseUserCredentials(parsed),
|
|
119
|
+
agent: parseAgentCredentials(parsed),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function parseAgentCredentials(parsed) {
|
|
127
|
+
const raw = parsed.agent;
|
|
128
|
+
if (!raw || typeof raw !== "object")
|
|
129
|
+
return undefined;
|
|
130
|
+
const block = raw;
|
|
131
|
+
const dynamic = parseAgentDynamicCredentials(block.dynamic);
|
|
132
|
+
const subAgents = parseSubAgentCredentials(block.subAgents);
|
|
133
|
+
if (!dynamic && !subAgents)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
...(dynamic ? { dynamic } : {}),
|
|
137
|
+
...(subAgents ? { subAgents } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function parseSubAgentCredentials(value) {
|
|
141
|
+
if (!value || typeof value !== "object")
|
|
142
|
+
return undefined;
|
|
143
|
+
const entries = value;
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const [key, raw] of Object.entries(entries)) {
|
|
146
|
+
const creds = parseAgentDynamicCredentials(raw);
|
|
147
|
+
if (creds)
|
|
148
|
+
result[key] = creds;
|
|
149
|
+
}
|
|
150
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
151
|
+
}
|
|
152
|
+
function parseAgentDynamicCredentials(value) {
|
|
153
|
+
if (!value || typeof value !== "object")
|
|
154
|
+
return undefined;
|
|
155
|
+
const v = value;
|
|
156
|
+
if (typeof v.clientId !== "string" || v.clientId.length === 0)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof v.projectUrl !== "string" || v.projectUrl.length === 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
const registeredAt = typeof v.registeredAt === "number" ? v.registeredAt : Math.floor(Date.now() / 1000);
|
|
161
|
+
return {
|
|
162
|
+
clientId: v.clientId,
|
|
163
|
+
clientSecret: typeof v.clientSecret === "string" ? v.clientSecret : undefined,
|
|
164
|
+
registrationAccessToken: typeof v.registrationAccessToken === "string" ? v.registrationAccessToken : undefined,
|
|
165
|
+
registrationClientUri: typeof v.registrationClientUri === "string" ? v.registrationClientUri : undefined,
|
|
166
|
+
registeredAt,
|
|
167
|
+
projectUrl: v.projectUrl,
|
|
168
|
+
harness: typeof v.harness === "string" ? v.harness : undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function parseUserCredentials(parsed) {
|
|
172
|
+
// Prefer the new nested shape; fall back to the legacy root-level
|
|
173
|
+
// `oauth2` block so configs written before the user/agent split still
|
|
174
|
+
// round-trip. The next saveConfig() drops the legacy field.
|
|
175
|
+
const nested = parsed.user && typeof parsed.user === "object"
|
|
176
|
+
? parsed.user
|
|
177
|
+
: undefined;
|
|
178
|
+
const oauth2 = parseOAuth2(nested?.oauth2 ?? parsed.oauth2);
|
|
179
|
+
if (!oauth2)
|
|
180
|
+
return undefined;
|
|
181
|
+
return { oauth2 };
|
|
182
|
+
}
|
|
183
|
+
function parseOAuth2(value) {
|
|
184
|
+
if (!value || typeof value !== "object")
|
|
185
|
+
return undefined;
|
|
186
|
+
const v = value;
|
|
187
|
+
if (typeof v.accessToken !== "string" || v.accessToken.length === 0)
|
|
188
|
+
return undefined;
|
|
189
|
+
if (typeof v.expiresAt !== "number")
|
|
190
|
+
return undefined;
|
|
191
|
+
return {
|
|
192
|
+
accessToken: v.accessToken,
|
|
193
|
+
refreshToken: typeof v.refreshToken === "string" ? v.refreshToken : undefined,
|
|
194
|
+
expiresAt: v.expiresAt,
|
|
195
|
+
subject: typeof v.subject === "string" ? v.subject : undefined,
|
|
196
|
+
clientId: typeof v.clientId === "string" ? v.clientId : undefined,
|
|
197
|
+
idToken: typeof v.idToken === "string" ? v.idToken : undefined,
|
|
198
|
+
scope: typeof v.scope === "string" ? v.scope : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Save config to the config file. Merges with existing values — only
|
|
203
|
+
* provided fields are overwritten. Concurrent processes serialize via a
|
|
204
|
+
* sibling lockfile and the write is atomic (write-temp + rename).
|
|
205
|
+
*
|
|
206
|
+
* To clear a field rather than ignore it, set it to `null` in `update`
|
|
207
|
+
* (e.g. `saveConfig({ user: null })`); `undefined` is treated as
|
|
208
|
+
* "leave alone" for backwards compatibility.
|
|
209
|
+
*/
|
|
210
|
+
function saveConfig(update) {
|
|
211
|
+
mutateConfig((existing) => mergeConfig(existing, update));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Read-modify-write the config under a sibling lockfile. The mutator
|
|
215
|
+
* receives the current on-disk config and must return the new config.
|
|
216
|
+
* Concurrent processes serialize on the lock; the write itself is
|
|
217
|
+
* atomic via write-temp + rename.
|
|
218
|
+
*/
|
|
219
|
+
function mutateConfig(mutator) {
|
|
220
|
+
ensureConfigDir();
|
|
221
|
+
const lock = acquireLock();
|
|
222
|
+
try {
|
|
223
|
+
const existing = loadConfig();
|
|
224
|
+
const next = mutator(existing);
|
|
225
|
+
writeConfigAtomic(next);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
releaseLock(lock);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function mergeConfig(existing, update) {
|
|
232
|
+
const result = { ...existing };
|
|
233
|
+
for (const key of Object.keys(update)) {
|
|
234
|
+
const value = update[key];
|
|
235
|
+
if (value === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
if (value === null) {
|
|
238
|
+
delete result[key];
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Type system can't track the per-key narrowing; the runtime is correct.
|
|
242
|
+
result[key] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
function ensureConfigDir() {
|
|
248
|
+
const dir = configDir();
|
|
249
|
+
if (!fs.existsSync(dir)) {
|
|
250
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function acquireLock() {
|
|
254
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
255
|
+
const lockPath = configLockFile();
|
|
256
|
+
for (;;) {
|
|
257
|
+
try {
|
|
258
|
+
const fd = fs.openSync(lockPath, "wx", 0o600);
|
|
259
|
+
fs.writeSync(fd, String(process.pid));
|
|
260
|
+
return { fd };
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (err.code !== "EEXIST")
|
|
264
|
+
throw err;
|
|
265
|
+
// Lock exists — check if it's stale.
|
|
266
|
+
try {
|
|
267
|
+
const stat = fs.statSync(lockPath);
|
|
268
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
269
|
+
// Best-effort break; if another process beats us, we'll retry.
|
|
270
|
+
try {
|
|
271
|
+
fs.unlinkSync(lockPath);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
/* ignore */
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Lockfile vanished between EEXIST and stat — retry immediately.
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (Date.now() > deadline) {
|
|
284
|
+
throw new Error(`Timed out waiting for config lock at ${lockPath}. ` +
|
|
285
|
+
"Another process may be holding it; remove the file if you are sure no other process is running.");
|
|
286
|
+
}
|
|
287
|
+
sleepSync(LOCK_POLL_MS);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
292
|
+
function sleepSync(ms) {
|
|
293
|
+
Atomics.wait(SLEEP_BUF, 0, 0, ms);
|
|
294
|
+
}
|
|
295
|
+
function releaseLock(lock) {
|
|
296
|
+
try {
|
|
297
|
+
fs.closeSync(lock.fd);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* ignore */
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
fs.unlinkSync(configLockFile());
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* ignore */
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function writeConfigAtomic(config) {
|
|
310
|
+
const target = configFile();
|
|
311
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
312
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
313
|
+
fs.renameSync(tmp, target);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Resolve config by checking environment variables first, then the config file.
|
|
317
|
+
* Returns the merged result with the source of each value.
|
|
318
|
+
*/
|
|
319
|
+
function resolveConfig() {
|
|
320
|
+
const file = loadConfig();
|
|
321
|
+
const envProjectUrl = process.env.ORY_PROJECT_URL;
|
|
322
|
+
const envApiKey = process.env.ORY_API_KEY;
|
|
323
|
+
return {
|
|
324
|
+
projectUrl: envProjectUrl ?? file.projectUrl,
|
|
325
|
+
apiKey: envApiKey ?? file.apiKey,
|
|
326
|
+
auditOnly: file.auditOnly === true,
|
|
327
|
+
projectUrlSource: envProjectUrl ? "env" : file.projectUrl ? "config" : "none",
|
|
328
|
+
apiKeySource: envApiKey ? "env" : file.apiKey ? "config" : "none",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Build the "not configured" prompt message for a given harness CLI bin name.
|
|
333
|
+
*/
|
|
334
|
+
function configPromptMessage(binName) {
|
|
335
|
+
return ("The Ory agent plugin is installed but not yet configured.\n" +
|
|
336
|
+
"\n" +
|
|
337
|
+
" Option 1 — Connect to Ory (enables authentication and permission checks):\n" +
|
|
338
|
+
` npx ${binName} configure --project-url <URL> --api-key <KEY>\n` +
|
|
339
|
+
"\n" +
|
|
340
|
+
" Option 2 — Continue without authentication (audit logging only):\n" +
|
|
341
|
+
` npx ${binName} configure --audit-only\n` +
|
|
342
|
+
"\n" +
|
|
343
|
+
`Configuration is saved to ${configFile()} and shared across all agent plugins.`);
|
|
344
|
+
}
|
package/dist/denial.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized denial messaging for tool calls blocked by Ory Permissions.
|
|
3
|
+
*
|
|
4
|
+
* All harness plugins format their deny reason / system message via these
|
|
5
|
+
* helpers so the human-facing text is consistent: tells the user which
|
|
6
|
+
* subject lacks which permission, and points them at their Ory Keto admin.
|
|
7
|
+
*
|
|
8
|
+
* For harnesses whose plugin contract has no native "block" signal
|
|
9
|
+
* (currently OpenCode), throw `OryDenialError` from the pre-tool hook —
|
|
10
|
+
* that aborts execution and surfaces `error.message` to the agent.
|
|
11
|
+
*/
|
|
12
|
+
import type { McpToolIdentifier } from "./mcp.js";
|
|
13
|
+
export interface DenialContext {
|
|
14
|
+
tool: string;
|
|
15
|
+
subjectId: string;
|
|
16
|
+
namespace?: string;
|
|
17
|
+
mcp?: McpToolIdentifier;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Span attributes that mark a denial as a security alert.
|
|
21
|
+
*
|
|
22
|
+
* Spread these onto the existing `tool.block` span attributes whenever an
|
|
23
|
+
* actual deny comes back from Ory. The shape is stable so downstream
|
|
24
|
+
* alerting rules can hook on `attributes.alert === true` and filter by
|
|
25
|
+
* `attributes.severity` or `attributes.alertKind` without string matching.
|
|
26
|
+
*
|
|
27
|
+
* The `blocked` field distinguishes harnesses whose plugin contract can
|
|
28
|
+
* stop the tool (claude-code/codex/gemini-cli exit-2, openclaw `{ block:
|
|
29
|
+
* true }`) from those that can only advise (opencode throws and relies on
|
|
30
|
+
* the harness to honor the exception). When `blocked === false` the agent
|
|
31
|
+
* may still execute the tool, so the alert is the only enforcement signal.
|
|
32
|
+
*/
|
|
33
|
+
export interface AlertAttributes {
|
|
34
|
+
alert: true;
|
|
35
|
+
severity: "high";
|
|
36
|
+
alertKind: "permission_denied";
|
|
37
|
+
blocked: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function alertAttributes(blocked: boolean): AlertAttributes;
|
|
40
|
+
/**
|
|
41
|
+
* Human-facing denial reason for an alert that the harness cannot enforce
|
|
42
|
+
* (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
|
|
43
|
+
* banner and notes that the trace span is the alert signal, so the agent
|
|
44
|
+
* — and any human reading transcripts — can't miss that a security event
|
|
45
|
+
* was emitted even though the tool was allowed to proceed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatAlertMessage(ctx: DenialContext): string;
|
|
48
|
+
/**
|
|
49
|
+
* Short alert summary suitable for compact UI fields (Gemini's
|
|
50
|
+
* systemMessage, etc.) when the denial cannot be enforced.
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatAlertSummary(ctx: DenialContext): string;
|
|
53
|
+
/**
|
|
54
|
+
* Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
|
|
55
|
+
* reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
|
|
56
|
+
* thrown error message).
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatDenialMessage(ctx: DenialContext): string;
|
|
59
|
+
/**
|
|
60
|
+
* Short denial summary suitable for compact UI fields (e.g. Gemini's
|
|
61
|
+
* systemMessage which appears inline in the agent's TUI).
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatDenialSummary(ctx: DenialContext): string;
|
|
64
|
+
/**
|
|
65
|
+
* Thrown by plugins whose hook contract has no native block signal and
|
|
66
|
+
* which fall back to aborting tool execution via an exception (OpenCode
|
|
67
|
+
* pattern). Because the harness may or may not honor the throw, the
|
|
68
|
+
* message uses `formatAlertMessage()` so the agent — and any human
|
|
69
|
+
* reading transcripts — sees explicit "ORY SECURITY ALERT TRIGGERED"
|
|
70
|
+
* text. The `code` field stays stable for callers/tests.
|
|
71
|
+
*/
|
|
72
|
+
export declare class OryDenialError extends Error {
|
|
73
|
+
readonly code: "ory_permission_denied";
|
|
74
|
+
readonly tool: string;
|
|
75
|
+
readonly subjectId: string;
|
|
76
|
+
readonly namespace?: string;
|
|
77
|
+
readonly mcp?: McpToolIdentifier;
|
|
78
|
+
constructor(ctx: DenialContext);
|
|
79
|
+
}
|
package/dist/denial.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Standardized denial messaging for tool calls blocked by Ory Permissions.
|
|
4
|
+
*
|
|
5
|
+
* All harness plugins format their deny reason / system message via these
|
|
6
|
+
* helpers so the human-facing text is consistent: tells the user which
|
|
7
|
+
* subject lacks which permission, and points them at their Ory Keto admin.
|
|
8
|
+
*
|
|
9
|
+
* For harnesses whose plugin contract has no native "block" signal
|
|
10
|
+
* (currently OpenCode), throw `OryDenialError` from the pre-tool hook —
|
|
11
|
+
* that aborts execution and surfaces `error.message` to the agent.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.OryDenialError = void 0;
|
|
15
|
+
exports.alertAttributes = alertAttributes;
|
|
16
|
+
exports.formatAlertMessage = formatAlertMessage;
|
|
17
|
+
exports.formatAlertSummary = formatAlertSummary;
|
|
18
|
+
exports.formatDenialMessage = formatDenialMessage;
|
|
19
|
+
exports.formatDenialSummary = formatDenialSummary;
|
|
20
|
+
const ADMIN_HINT = "Contact your Ory Keto administrator to grant the required permission.";
|
|
21
|
+
function alertAttributes(blocked) {
|
|
22
|
+
return {
|
|
23
|
+
alert: true,
|
|
24
|
+
severity: "high",
|
|
25
|
+
alertKind: "permission_denied",
|
|
26
|
+
blocked,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const ALERT_PREFIX = "ORY SECURITY ALERT TRIGGERED";
|
|
30
|
+
/**
|
|
31
|
+
* Human-facing denial reason for an alert that the harness cannot enforce
|
|
32
|
+
* (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
|
|
33
|
+
* banner and notes that the trace span is the alert signal, so the agent
|
|
34
|
+
* — and any human reading transcripts — can't miss that a security event
|
|
35
|
+
* was emitted even though the tool was allowed to proceed.
|
|
36
|
+
*/
|
|
37
|
+
function formatAlertMessage(ctx) {
|
|
38
|
+
return `[${ALERT_PREFIX}] ${formatDenialMessage(ctx)} A security alert trace span was recorded for this denied tool call.`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Short alert summary suitable for compact UI fields (Gemini's
|
|
42
|
+
* systemMessage, etc.) when the denial cannot be enforced.
|
|
43
|
+
*/
|
|
44
|
+
function formatAlertSummary(ctx) {
|
|
45
|
+
return `[${ALERT_PREFIX}] ${formatDenialSummary(ctx)}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
|
|
49
|
+
* reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
|
|
50
|
+
* thrown error message).
|
|
51
|
+
*/
|
|
52
|
+
function formatDenialMessage(ctx) {
|
|
53
|
+
if (ctx.mcp) {
|
|
54
|
+
const tool = ctx.mcp.toolName
|
|
55
|
+
? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
|
|
56
|
+
: ctx.mcp.serverName;
|
|
57
|
+
return (`Ory: permission denied for MCP server "${ctx.mcp.serverName}". ` +
|
|
58
|
+
`Subject "${ctx.subjectId}" is not authorized to use "${tool}". ` +
|
|
59
|
+
ADMIN_HINT);
|
|
60
|
+
}
|
|
61
|
+
const ns = ctx.namespace ?? "AgentTools";
|
|
62
|
+
return (`Ory: permission denied for "${ctx.tool}". ` +
|
|
63
|
+
`Subject "${ctx.subjectId}" is not authorized to invoke "${ctx.tool}" ` +
|
|
64
|
+
`(namespace "${ns}"). ` +
|
|
65
|
+
ADMIN_HINT);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Short denial summary suitable for compact UI fields (e.g. Gemini's
|
|
69
|
+
* systemMessage which appears inline in the agent's TUI).
|
|
70
|
+
*/
|
|
71
|
+
function formatDenialSummary(ctx) {
|
|
72
|
+
if (ctx.mcp) {
|
|
73
|
+
const tool = ctx.mcp.toolName
|
|
74
|
+
? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
|
|
75
|
+
: ctx.mcp.serverName;
|
|
76
|
+
return `Ory: permission denied for MCP tool "${tool}". ${ADMIN_HINT}`;
|
|
77
|
+
}
|
|
78
|
+
return `Ory: permission denied for "${ctx.tool}". ${ADMIN_HINT}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Thrown by plugins whose hook contract has no native block signal and
|
|
82
|
+
* which fall back to aborting tool execution via an exception (OpenCode
|
|
83
|
+
* pattern). Because the harness may or may not honor the throw, the
|
|
84
|
+
* message uses `formatAlertMessage()` so the agent — and any human
|
|
85
|
+
* reading transcripts — sees explicit "ORY SECURITY ALERT TRIGGERED"
|
|
86
|
+
* text. The `code` field stays stable for callers/tests.
|
|
87
|
+
*/
|
|
88
|
+
class OryDenialError extends Error {
|
|
89
|
+
code = "ory_permission_denied";
|
|
90
|
+
tool;
|
|
91
|
+
subjectId;
|
|
92
|
+
namespace;
|
|
93
|
+
mcp;
|
|
94
|
+
constructor(ctx) {
|
|
95
|
+
super(formatAlertMessage(ctx));
|
|
96
|
+
this.name = "OryDenialError";
|
|
97
|
+
this.tool = ctx.tool;
|
|
98
|
+
this.subjectId = ctx.subjectId;
|
|
99
|
+
this.namespace = ctx.namespace;
|
|
100
|
+
this.mcp = ctx.mcp;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.OryDenialError = OryDenialError;
|