@ory/antigravity 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,7 +48,7 @@ npx -y -p @ory/antigravity ory-antigravity status
48
48
  npx -y -p @ory/antigravity ory-antigravity uninstall
49
49
  ```
50
50
 
51
- If the `agy` binary isn't on your `PATH`, `npx -y -p @ory/antigravity ory-antigravity-setup` writes the plugin bundle directly into your project's `.agents/` directory (`plugin.json`, `hooks.json`, `mcp_config.json`, `skills/`, `commands/`).
51
+ If the `agy` binary isn't on your `PATH`, `npx -y -p @ory/antigravity ory-antigravity-setup` writes the plugin bundle directly into your project's `.agents/` directory (`plugin.json`, `hooks.json`, `mcp_config.json`, `skills/`, `commands/`). Because `.agents/` is shared with your own configuration, the setup merges rather than overwrites: it only sets or replaces the Ory-owned entries in `hooks.json` and `mcp_config.json`, leaving your hook rules and MCP servers intact, and it never touches a `plugin.json` that isn't Ory's own. Uninstall is the mirror image — it removes only the Ory-owned entries and deletes a file only once nothing else is left in it.
52
52
 
53
53
  </details>
54
54
 
@@ -23,7 +23,21 @@ export declare const PLUGIN_NAME = "ory";
23
23
  * Write the full Ory plugin bundle under `root`. Used both for the
24
24
  * bundle handed to `agy plugin install` and for the manual `.agents`
25
25
  * fallback.
26
+ *
27
+ * The manual fallback targets the workspace's shared `.agents/` directory,
28
+ * where users keep their own hook rules and MCP servers — so the JSON
29
+ * files are MERGED, never whole-file overwritten: only the Ory-owned key
30
+ * is set/replaced, everything else is preserved. (In the package-owned
31
+ * bundle dir the merge is a no-op over a fresh tree, so the same code
32
+ * path is used everywhere.)
26
33
  */
27
34
  export declare function installAntigravityOryAssets(root: string): void;
28
- /** Remove every Ory skill, command, and bundle file written under `root`. */
35
+ /**
36
+ * Remove every Ory skill, command, and bundle entry written under `root`.
37
+ *
38
+ * Mirror of the merge-aware install: only Ory-owned keys are removed from
39
+ * `hooks.json` / `mcp_config.json`, and each file is deleted only when it
40
+ * becomes empty. `plugin.json` is deleted only when it is Ory's own
41
+ * manifest. Unparseable files are left untouched.
42
+ */
29
43
  export declare function uninstallAntigravityOryAssets(root: string): void;
@@ -126,16 +126,47 @@ function renderMcpConfig(version) {
126
126
  },
127
127
  };
128
128
  }
129
+ /**
130
+ * Read a JSON file as a plain object. Returns `undefined` when the file
131
+ * is missing, unparseable, or not a JSON object — callers treat all three
132
+ * as "nothing mergeable here".
133
+ */
134
+ function readJsonObject(file) {
135
+ try {
136
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
137
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
138
+ return parsed;
139
+ }
140
+ }
141
+ catch {
142
+ /* missing or unparseable */
143
+ }
144
+ return undefined;
145
+ }
146
+ function writeJson(file, obj) {
147
+ fs.writeFileSync(file, JSON.stringify(obj, null, 2) + "\n");
148
+ }
149
+ /** True when a parsed plugin.json manifest is Ory's own. */
150
+ function isOryManifest(manifest) {
151
+ return manifest?.name === exports.PLUGIN_NAME;
152
+ }
129
153
  /**
130
154
  * Write the full Ory plugin bundle under `root`. Used both for the
131
155
  * bundle handed to `agy plugin install` and for the manual `.agents`
132
156
  * fallback.
157
+ *
158
+ * The manual fallback targets the workspace's shared `.agents/` directory,
159
+ * where users keep their own hook rules and MCP servers — so the JSON
160
+ * files are MERGED, never whole-file overwritten: only the Ory-owned key
161
+ * is set/replaced, everything else is preserved. (In the package-owned
162
+ * bundle dir the merge is a no-op over a fresh tree, so the same code
163
+ * path is used everywhere.)
133
164
  */
134
165
  function installAntigravityOryAssets(root) {
135
166
  const version = readPackageVersion();
136
167
  fs.mkdirSync(root, { recursive: true });
137
168
  const skillsDir = path.join(root, "skills");
138
- fs.rmSync(skillsDir, { recursive: true, force: true });
169
+ (0, argus_1.removeSkillDirs)(skillsDir, argus_1.ORY_SKILL_NAMES);
139
170
  (0, argus_1.writeSkillTree)(skillsDir, (0, argus_1.renderOrySkills)("antigravity", RENDER_OPTS));
140
171
  const cmdDir = path.join(root, "commands", CMD_SUBDIR);
141
172
  fs.rmSync(cmdDir, { recursive: true, force: true });
@@ -143,9 +174,31 @@ function installAntigravityOryAssets(root) {
143
174
  for (const cmd of (0, argus_1.renderOryCommands)("antigravity", RENDER_OPTS)) {
144
175
  fs.writeFileSync(path.join(cmdDir, `${cmd.slug}.toml`), (0, argus_1.commandToToml)(cmd));
145
176
  }
146
- fs.writeFileSync(path.join(root, "hooks.json"), JSON.stringify(renderHooks(version), null, 2) + "\n");
147
- fs.writeFileSync(path.join(root, "mcp_config.json"), JSON.stringify(renderMcpConfig(version), null, 2) + "\n");
148
- fs.writeFileSync(path.join(root, "plugin.json"), JSON.stringify(renderManifest(version), null, 2) + "\n");
177
+ // hooks.json is keyed by rule id — replace only the "ory" rule and keep
178
+ // any rules the user has defined alongside it.
179
+ const hooksPath = path.join(root, "hooks.json");
180
+ const hooks = readJsonObject(hooksPath) ?? {};
181
+ hooks[exports.PLUGIN_NAME] = renderHooks(version)[exports.PLUGIN_NAME];
182
+ writeJson(hooksPath, hooks);
183
+ // mcp_config.json — replace only the Ory server entry under `mcpServers`,
184
+ // preserving other servers and any unrelated top-level keys.
185
+ const mcpPath = path.join(root, "mcp_config.json");
186
+ const mcp = readJsonObject(mcpPath) ?? {};
187
+ const existingServers = mcp.mcpServers;
188
+ const servers = existingServers && typeof existingServers === "object" && !Array.isArray(existingServers)
189
+ ? existingServers
190
+ : {};
191
+ servers[exports.PLUGIN_NAME] = renderMcpConfig(version).mcpServers[exports.PLUGIN_NAME];
192
+ mcp.mcpServers = servers;
193
+ writeJson(mcpPath, mcp);
194
+ // plugin.json — only write over a manifest that is Ory's own (or absent).
195
+ const manifestPath = path.join(root, "plugin.json");
196
+ if (!fs.existsSync(manifestPath) || isOryManifest(readJsonObject(manifestPath))) {
197
+ writeJson(manifestPath, renderManifest(version));
198
+ }
199
+ else {
200
+ console.warn(`Warning: ${manifestPath} exists and is not the Ory plugin manifest; leaving it untouched.`);
201
+ }
149
202
  }
150
203
  function safeRmEmpty(dir) {
151
204
  try {
@@ -157,7 +210,14 @@ function safeRmEmpty(dir) {
157
210
  /* leave the dir if removal fails */
158
211
  }
159
212
  }
160
- /** Remove every Ory skill, command, and bundle file written under `root`. */
213
+ /**
214
+ * Remove every Ory skill, command, and bundle entry written under `root`.
215
+ *
216
+ * Mirror of the merge-aware install: only Ory-owned keys are removed from
217
+ * `hooks.json` / `mcp_config.json`, and each file is deleted only when it
218
+ * becomes empty. `plugin.json` is deleted only when it is Ory's own
219
+ * manifest. Unparseable files are left untouched.
220
+ */
161
221
  function uninstallAntigravityOryAssets(root) {
162
222
  const skillsDir = path.join(root, "skills");
163
223
  if (fs.existsSync(skillsDir)) {
@@ -174,9 +234,42 @@ function uninstallAntigravityOryAssets(root) {
174
234
  safeRmEmpty(cmdDir);
175
235
  safeRmEmpty(path.join(root, "commands"));
176
236
  }
177
- for (const f of ["hooks.json", "mcp_config.json", "plugin.json"]) {
178
- const p = path.join(root, f);
179
- if (fs.existsSync(p))
180
- fs.unlinkSync(p);
237
+ // hooks.json drop only the Ory rule id.
238
+ const hooksPath = path.join(root, "hooks.json");
239
+ const hooks = readJsonObject(hooksPath);
240
+ if (hooks && exports.PLUGIN_NAME in hooks) {
241
+ delete hooks[exports.PLUGIN_NAME];
242
+ if (Object.keys(hooks).length === 0) {
243
+ fs.unlinkSync(hooksPath);
244
+ }
245
+ else {
246
+ writeJson(hooksPath, hooks);
247
+ }
248
+ }
249
+ // mcp_config.json — drop only the Ory server entry.
250
+ const mcpPath = path.join(root, "mcp_config.json");
251
+ const mcp = readJsonObject(mcpPath);
252
+ if (mcp) {
253
+ const servers = mcp.mcpServers;
254
+ if (servers &&
255
+ typeof servers === "object" &&
256
+ !Array.isArray(servers) &&
257
+ exports.PLUGIN_NAME in servers) {
258
+ delete servers[exports.PLUGIN_NAME];
259
+ if (Object.keys(servers).length === 0) {
260
+ delete mcp.mcpServers;
261
+ }
262
+ if (Object.keys(mcp).length === 0) {
263
+ fs.unlinkSync(mcpPath);
264
+ }
265
+ else {
266
+ writeJson(mcpPath, mcp);
267
+ }
268
+ }
269
+ }
270
+ // plugin.json — delete only when it is Ory's own manifest.
271
+ const manifestPath = path.join(root, "plugin.json");
272
+ if (fs.existsSync(manifestPath) && isOryManifest(readJsonObject(manifestPath))) {
273
+ fs.unlinkSync(manifestPath);
181
274
  }
182
275
  }
package/dist/emit.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Write-once stdout emitter for the Antigravity hook.
3
+ *
4
+ * Antigravity parses the hook's stdout as a single JSON document and
5
+ * FAILS CLOSED on malformed output. If two code paths both write (e.g.
6
+ * the main path writes the decision and then `tracer.shutdown()` rejects,
7
+ * landing in the top-level catch which writes FAIL_OPEN again), stdout
8
+ * carries two concatenated JSON documents and the harness blocks the
9
+ * tool. This factory guarantees exactly one write ever reaches stdout.
10
+ */
11
+ export type EmitOnce<T> = (output: T) => boolean;
12
+ /**
13
+ * Create an emitter that serializes `output` as JSON and writes it via
14
+ * `write` on the first call only. Subsequent calls are no-ops that
15
+ * return `false`.
16
+ */
17
+ export declare function createEmitOnce<T>(write?: (chunk: string) => void): EmitOnce<T>;
package/dist/emit.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Write-once stdout emitter for the Antigravity hook.
4
+ *
5
+ * Antigravity parses the hook's stdout as a single JSON document and
6
+ * FAILS CLOSED on malformed output. If two code paths both write (e.g.
7
+ * the main path writes the decision and then `tracer.shutdown()` rejects,
8
+ * landing in the top-level catch which writes FAIL_OPEN again), stdout
9
+ * carries two concatenated JSON documents and the harness blocks the
10
+ * tool. This factory guarantees exactly one write ever reaches stdout.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.createEmitOnce = createEmitOnce;
14
+ /**
15
+ * Create an emitter that serializes `output` as JSON and writes it via
16
+ * `write` on the first call only. Subsequent calls are no-ops that
17
+ * return `false`.
18
+ */
19
+ function createEmitOnce(write = (chunk) => {
20
+ process.stdout.write(chunk);
21
+ }) {
22
+ let wrote = false;
23
+ return (output) => {
24
+ if (wrote)
25
+ return false;
26
+ wrote = true;
27
+ write(JSON.stringify(output));
28
+ return true;
29
+ };
30
+ }
package/dist/hook.js CHANGED
@@ -18,9 +18,18 @@
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  const argus_1 = require("@ory/argus");
21
+ const emit_js_1 = require("./emit.js");
21
22
  const handlers_js_1 = require("./handlers.js");
22
23
  /** The fail-open response: explicitly allow, so a crash never blocks. */
23
24
  const FAIL_OPEN = { allow_tool: true };
25
+ /**
26
+ * Single write-once gate shared by every exit path (normal, parse
27
+ * failure, and the top-level catch). Because Antigravity fails closed on
28
+ * malformed stdout, a double write (two concatenated JSON documents)
29
+ * would BLOCK the tool — e.g. when the main path writes its decision and
30
+ * `tracer.shutdown()` then rejects into the top-level catch.
31
+ */
32
+ const emit = (0, emit_js_1.createEmitOnce)();
24
33
  /**
25
34
  * Read all of stdin as a string. Uses event listeners (not `for await`)
26
35
  * with an idle timer so we don't hang if the parent never sends EOF.
@@ -60,7 +69,7 @@ async function main() {
60
69
  catch {
61
70
  client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
62
71
  // Can't tell the event; allow to avoid wedging a PreToolUse call.
63
- process.stdout.write(JSON.stringify(FAIL_OPEN));
72
+ emit(FAIL_OPEN);
64
73
  await client.tracer.shutdown();
65
74
  process.exit(0);
66
75
  }
@@ -75,13 +84,15 @@ async function main() {
75
84
  });
76
85
  output = FAIL_OPEN;
77
86
  }
78
- process.stdout.write(JSON.stringify(output));
87
+ emit(output);
79
88
  await client.tracer.shutdown();
80
89
  process.exit(0);
81
90
  }
82
91
  main().catch((err) => {
83
- // Last-resort guard: allow rather than block, then exit cleanly.
92
+ // Last-resort guard: allow rather than block, then exit cleanly. The
93
+ // write-once emitter makes this a no-op when the decision was already
94
+ // written before the rejection (e.g. a failing tracer shutdown).
84
95
  process.stderr.write(`[ory-agent] fatal: ${err}\n`);
85
- process.stdout.write(JSON.stringify(FAIL_OPEN));
96
+ emit(FAIL_OPEN);
86
97
  process.exit(0);
87
98
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/antigravity",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Ory plugin for Google Antigravity: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/ory/ory-agent-plugins/tree/main/packages/antigravity",
@@ -72,7 +72,7 @@
72
72
  "!dist/**/*.tsbuildinfo"
73
73
  ],
74
74
  "dependencies": {
75
- "@ory/argus": "0.10.0"
75
+ "@ory/argus": "0.11.0"
76
76
  },
77
77
  "devDependencies": {
78
78
  "typescript": "^6.0.2",