@openape/nest 0.2.2 → 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.
Files changed (3) hide show
  1. package/README.md +106 -0
  2. package/dist/index.mjs +207 -89
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @openape/nest
2
+
3
+ Local control-plane daemon. Manages OpenApe agents on a single machine — provisions macOS users, hands the bridge lifecycle off to launchd, exposes a localhost HTTP API gated by DDISA grant tokens.
4
+
5
+ ## What it does
6
+
7
+ - Runs as a long-lived launchd-managed daemon under your user account (`~/Library/LaunchAgents/ai.openape.nest.plist`)
8
+ - Accepts API calls on `127.0.0.1:9091` for agent lifecycle ops (`spawn`, `destroy`, `list`, `status`)
9
+ - Every API call requires a DDISA-signed grant token in the `Authorization: Bearer …` header
10
+ - Bridge processes (`openape-chat-bridge` per agent) are NOT supervised in-daemon — they run as system-domain LaunchDaemons (one plist per agent in `/Library/LaunchDaemons/eco.hofmann.apes.bridge.<agent>.plist`) installed by `apes agents spawn --bridge`. launchd is the right OS-level supervisor on macOS; trying to duplicate that in the daemon crashloops without adding value.
11
+
12
+ ## Setup (one-time)
13
+
14
+ ```bash
15
+ apes nest install # install + load the launchd plist
16
+ apes nest enroll # daemon gets its own DDISA agent identity
17
+ apes nest authorize # set the YOLO policy — covers the inner
18
+ # `apes agents spawn` calls the daemon makes
19
+ ```
20
+
21
+ Set the bridge default model at install time so every spawn uses the
22
+ same one without `--bridge-model` repetition:
23
+
24
+ ```bash
25
+ apes nest install --bridge-model gpt-5.4 # ChatGPT-only LiteLLM proxy
26
+ apes nest install --bridge-model claude-haiku-4-5 # Anthropic
27
+ ```
28
+
29
+ This writes `APE_CHAT_BRIDGE_MODEL=<value>` to `~/litellm/.env`,
30
+ which `resolveBridgeConfig()` reads at every `apes [nest|agents] spawn
31
+ --bridge`. Re-run with a different value to overwrite.
32
+
33
+ ### Optional: privilege isolation with a dedicated service user
34
+
35
+ By default, `apes nest install` configures the daemon as a user-domain
36
+ `LaunchAgent` running under your own Mac user account, with state at
37
+ `~/.openape/nest`. That works fine for personal use. For a more
38
+ hardened setup the daemon can be promoted to a system-domain
39
+ `LaunchDaemon` running under a dedicated `_openape_nest` macOS service
40
+ user (uid 481, hidden, no shell, no GUI session) with state under
41
+ `/var/openape/nest`.
42
+
43
+ To migrate an existing user-domain install:
44
+
45
+ ```bash
46
+ apes run --as root --wait -- bash apps/openape-nest/scripts/migrate-to-service-user.sh
47
+ ```
48
+
49
+ The script creates the user/group, copies your data dir to
50
+ `/var/openape/nest`, and swaps the plist. The Nest's IdP identity is
51
+ bound to its ssh keypair (which moves with the data dir), so the same
52
+ `nest-…@id.openape.ai` identity continues to work — no re-enroll
53
+ needed, all existing approved delegations / grants stay valid.
54
+
55
+ After migration you may want a fresh `apes login --key` for the Nest
56
+ to refresh the access token (the migrated `auth.json` carries the
57
+ old token; `cli-auth`'s challenge-response refresh handles it on
58
+ expiry, but a manual login also works).
59
+
60
+ After that, day-to-day lifecycle goes through `apes nest`:
61
+
62
+ ```bash
63
+ apes nest spawn igor18 # provision a new agent
64
+ apes nest list # show agents this nest knows about
65
+ apes nest status # health-check
66
+ apes nest destroy igor18 # tear down
67
+ apes nest uninstall # remove the launchd plist
68
+ ```
69
+
70
+ ## Why every API call needs a grant token
71
+
72
+ Without auth the API is gated only by "process running as the logged-in human can reach localhost:9091" — a compromised local process inherits everything. The grant-token requirement closes that gap and gives every call an audit record at the IdP. The flow:
73
+
74
+ 1. `apes nest <op>` looks for an existing approved `'always'`/`'timed'` grant matching the operation.
75
+ 2. If none, requests a fresh grant from the IdP. **First-time** grants for human callers wait for human approval (one approval covers the lifetime of the grant — `'always'` is the default for nest-CLI calls).
76
+ 3. Token (RFC-7519 JWT signed by the IdP) is fetched and presented as `Authorization: Bearer …`.
77
+ 4. The Nest verifies signature against the IdP's JWKS, checks `aud=nest`, `iss=<IdP URL>`, `target_host=<local hostname>`, and exact-matches the embedded `command` claim against the route. Fails: `401` (auth) or `403` (command mismatch).
78
+
79
+ ### Grant-scope conventions
80
+
81
+ | CLI | Grant `command` | Reuse semantics |
82
+ |---|---|---|
83
+ | `apes nest list` | `["nest","list"]` | One approval, reused forever. |
84
+ | `apes nest status` | `["nest","status"]` | One approval, reused forever. |
85
+ | `apes nest spawn <name>` | `["nest","spawn"]` (no name baked in) | One approval, any future spawn. Trade-off: a compromised local process running as the human can spawn arbitrary agents under that grant. Acceptable because spawn is reversible and audited. |
86
+ | `apes nest destroy <name>` | `["nest","destroy","<name>"]` | Per-name. Destroying each agent is its own approval. Destructive ops keep tighter scoping by design. |
87
+
88
+ Direct `curl` to the API is supported but you must fetch a grant token yourself. See `tests/auth-negative.sh` for an example token-fetch.
89
+
90
+ ## Negative-test smoke
91
+
92
+ ```bash
93
+ bash apps/openape-nest/tests/auth-negative.sh
94
+ ```
95
+
96
+ Verifies: no-bearer → 401, garbage-bearer → 401, wrong-audience → 401, command-mismatch → 403.
97
+
98
+ ## Why no in-daemon bridge supervisor
99
+
100
+ Earlier versions ran a per-agent process supervisor inside the Nest daemon to keep `openape-chat-bridge` instances alive. It was removed because:
101
+
102
+ - `apes agents spawn --bridge` already installs a system-domain `LaunchDaemon` per agent. launchd KeepAlive's it as the agent UID with the right PATH (the bridge binary is at `/Users/<agent>/.bun/bin/openape-chat-bridge`).
103
+ - The supervisor's children inherited the daemon's PATH (the human user's PATH, which doesn't include any agent's `~/.bun/bin`), so they crashlooped on `Command not found: openape-chat-bridge` while the launchd-domain bridge ran fine.
104
+ - Each crashloop produced an auto-approved YOLO grant — pushing one notification per cycle.
105
+
106
+ Single-source-of-truth on launchd. The Nest is now an API surface in front of `apes agents spawn|destroy`, nothing more.
package/dist/index.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createServer } from "http";
5
- import process from "process";
4
+ import process3 from "process";
5
+
6
+ // src/lib/intent-channel.ts
7
+ import { readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
8
+ import { homedir as homedir2 } from "os";
9
+ import { join as join2 } from "path";
6
10
 
7
11
  // src/api/agents.ts
8
12
  import { execFile } from "child_process";
@@ -12,7 +16,7 @@ import { promisify } from "util";
12
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
13
17
  import { homedir } from "os";
14
18
  import { join } from "path";
15
- var REGISTRY_DIR = join(homedir(), ".openape", "nest");
19
+ var REGISTRY_DIR = homedir();
16
20
  var REGISTRY_PATH = join(REGISTRY_DIR, "agents.json");
17
21
  function emptyRegistry() {
18
22
  return { version: 1, agents: [] };
@@ -57,15 +61,6 @@ function removeAgent(name) {
57
61
  // src/api/agents.ts
58
62
  var execFileAsync = promisify(execFile);
59
63
  var NAME_REGEX = /^[a-z][a-z0-9-]{0,23}$/;
60
- function handleNestStatus(ctx) {
61
- return {
62
- agents: listAgents().length,
63
- processes: ctx.supervisor.status()
64
- };
65
- }
66
- function handleAgentsList(_ctx) {
67
- return { agents: listAgents() };
68
- }
69
64
  async function handleAgentSpawn(ctx) {
70
65
  const body = ctx.body;
71
66
  const name = typeof body?.name === "string" ? body.name : "";
@@ -76,7 +71,7 @@ async function handleAgentSpawn(ctx) {
76
71
  throw new Error(`agent "${name}" is already registered with this nest`);
77
72
  }
78
73
  const args = ["run", "--as", "root", "--wait", "--", "apes", "agents", "spawn", name];
79
- const includeBridge = body?.bridge === true;
74
+ const includeBridge = body?.bridge !== false;
80
75
  if (includeBridge) {
81
76
  args.push("--bridge");
82
77
  if (typeof body?.bridgeKey === "string") args.push("--bridge-key", body.bridgeKey);
@@ -125,34 +120,144 @@ async function readUidFromDscl(name) {
125
120
  return -1;
126
121
  }
127
122
 
123
+ // src/lib/intent-channel.ts
124
+ var POLL_MS = 1e3;
125
+ var INTENTS_DIR = join2(homedir2(), "intents");
126
+ var IntentChannel = class {
127
+ constructor(deps) {
128
+ this.deps = deps;
129
+ mkdirSync2(INTENTS_DIR, { recursive: true });
130
+ chmodSync(INTENTS_DIR, 504);
131
+ }
132
+ timer;
133
+ inflight = /* @__PURE__ */ new Set();
134
+ start() {
135
+ if (this.timer) return;
136
+ this.timer = setInterval(() => void this.tick(), POLL_MS);
137
+ this.deps.log(`intent-channel: polling ${INTENTS_DIR}`);
138
+ }
139
+ stop() {
140
+ if (this.timer) clearInterval(this.timer);
141
+ this.timer = void 0;
142
+ }
143
+ async tick() {
144
+ let entries;
145
+ try {
146
+ entries = readdirSync(INTENTS_DIR);
147
+ } catch {
148
+ return;
149
+ }
150
+ for (const f of entries) {
151
+ if (!f.endsWith(".json")) continue;
152
+ if (this.inflight.has(f)) continue;
153
+ this.inflight.add(f);
154
+ void this.process(f).finally(() => this.inflight.delete(f));
155
+ }
156
+ }
157
+ async process(filename) {
158
+ const path = join2(INTENTS_DIR, filename);
159
+ let intent;
160
+ try {
161
+ const raw = readFileSync2(path, "utf8");
162
+ intent = JSON.parse(raw);
163
+ } catch (err) {
164
+ this.deps.log(`intent-channel: failed to read ${filename}: ${err instanceof Error ? err.message : String(err)}`);
165
+ try {
166
+ unlinkSync(path);
167
+ } catch {
168
+ }
169
+ return;
170
+ }
171
+ this.deps.log(`intent-channel: processing ${intent.action} (id=${intent.id})`);
172
+ let response;
173
+ try {
174
+ const ctx = {
175
+ url: new URL("intent:/"),
176
+ body: intent,
177
+ log: this.deps.log,
178
+ apesBin: this.deps.apesBin,
179
+ caller: "<intent-channel>",
180
+ grantId: intent.id,
181
+ supervisor: this.deps.supervisor
182
+ };
183
+ let result;
184
+ switch (intent.action) {
185
+ case "spawn":
186
+ result = await handleAgentSpawn(ctx);
187
+ break;
188
+ case "destroy":
189
+ result = await handleAgentDestroy(ctx, intent.name);
190
+ break;
191
+ case "list":
192
+ result = { agents: listAgents() };
193
+ break;
194
+ default:
195
+ throw new Error(`unknown action: ${intent.action ?? "<undefined>"}`);
196
+ }
197
+ response = { ok: true, result };
198
+ } catch (err) {
199
+ const msg = err instanceof Error ? err.message : String(err);
200
+ this.deps.log(`intent-channel: ${intent.action} failed: ${msg}`);
201
+ response = { ok: false, error: msg };
202
+ }
203
+ const respTmp = `${path.replace(/\.json$/, "")}.response.tmp`;
204
+ const respFinal = `${path.replace(/\.json$/, "")}.response`;
205
+ writeFileSync2(respTmp, `${JSON.stringify(response)}
206
+ `, { mode: 432 });
207
+ renameSync(respTmp, respFinal);
208
+ try {
209
+ unlinkSync(path);
210
+ } catch {
211
+ }
212
+ }
213
+ };
214
+ function reapStaleResponses(log2) {
215
+ let entries;
216
+ try {
217
+ entries = readdirSync(INTENTS_DIR);
218
+ } catch {
219
+ return;
220
+ }
221
+ const now = Date.now();
222
+ for (const f of entries) {
223
+ if (!f.endsWith(".response")) continue;
224
+ const path = join2(INTENTS_DIR, f);
225
+ try {
226
+ const st = statSync(path);
227
+ if (now - st.mtimeMs > 60 * 60 * 1e3) {
228
+ unlinkSync(path);
229
+ log2(`intent-channel: reaped stale ${f}`);
230
+ }
231
+ } catch {
232
+ }
233
+ }
234
+ }
235
+
128
236
  // src/lib/supervisor.ts
129
237
  import { spawn } from "child_process";
130
- var MIN_BACKOFF_MS = 1e3;
238
+ import process from "process";
239
+ var MIN_BACKOFF_MS = 2e3;
131
240
  var MAX_BACKOFF_MS = 6e4;
241
+ var STABLE_RUNTIME_MS = 3e4;
132
242
  var Supervisor = class {
133
243
  constructor(deps) {
134
244
  this.deps = deps;
135
245
  }
136
246
  children = /* @__PURE__ */ new Map();
137
- /**
138
- * Bring the supervised set in line with the desired set. Spawns
139
- * agents that aren't running, kills agents that are no longer in
140
- * the registry. Idempotent — call after every registry mutation.
141
- */
247
+ /** Bring the supervised set in line with the desired set. */
142
248
  reconcile(desired) {
143
- const desiredNames = new Set(desired.map((a) => a.name));
249
+ const desiredNames = new Set(desired.filter((a) => a.bridge != null).map((a) => a.name));
144
250
  for (const [name] of this.children) {
145
251
  if (!desiredNames.has(name)) this.stop(name);
146
252
  }
147
253
  for (const agent of desired) {
254
+ if (agent.bridge == null) continue;
148
255
  if (!this.children.has(agent.name)) this.start(agent);
149
256
  }
150
257
  }
151
- /** Number of currently-running supervised processes. */
152
258
  size() {
153
259
  return this.children.size;
154
260
  }
155
- /** Snapshot of supervised state — useful for /agents GET. */
156
261
  status() {
157
262
  const now = Date.now();
158
263
  return Array.from(this.children.entries()).map(([name, s]) => ({
@@ -164,13 +269,13 @@ var Supervisor = class {
164
269
  }
165
270
  start(agent) {
166
271
  if (this.children.has(agent.name)) return;
167
- this.deps.log(`supervisor: starting ${agent.name}`);
272
+ this.deps.log(`supervisor: starting bridge for ${agent.name}`);
168
273
  this.spawnChild(agent, 0);
169
274
  }
170
275
  stop(name) {
171
276
  const s = this.children.get(name);
172
277
  if (!s) return;
173
- this.deps.log(`supervisor: stopping ${name}`);
278
+ this.deps.log(`supervisor: stopping bridge for ${name}`);
174
279
  if (s.restartTimer) clearTimeout(s.restartTimer);
175
280
  this.children.delete(name);
176
281
  try {
@@ -178,15 +283,19 @@ var Supervisor = class {
178
283
  } catch {
179
284
  }
180
285
  }
181
- /** Kill all children — called on daemon shutdown. */
182
286
  stopAll() {
183
287
  for (const name of Array.from(this.children.keys())) this.stop(name);
184
288
  }
185
289
  spawnChild(agent, prevCrashes) {
186
- const args = ["run", "--as", agent.name, "--", "openape-chat-bridge"];
290
+ const args = ["run", "--as", agent.name, "--wait", "--", "openape-chat-bridge"];
187
291
  const child = spawn(this.deps.apesBin, args, {
188
292
  stdio: ["ignore", "pipe", "pipe"],
189
- detached: false
293
+ detached: false,
294
+ // Inherit env — most importantly PATH (host bin dirs from
295
+ // captureHostBinDirs at install time) and HOME (the Nest's
296
+ // data dir, where its own auth.json lives so apes-cli reads
297
+ // the nest identity for the YOLO grant).
298
+ env: process.env
190
299
  });
191
300
  child.stdout?.on("data", (chunk) => this.forwardLog(agent.name, "stdout", chunk));
192
301
  child.stderr?.on("data", (chunk) => this.forwardLog(agent.name, "stderr", chunk));
@@ -199,11 +308,11 @@ var Supervisor = class {
199
308
  child.on("exit", (code, signal) => {
200
309
  const stillManaged = this.children.get(agent.name) === supervised;
201
310
  if (!stillManaged) return;
202
- const ranLongEnough = Date.now() - supervised.startedAt > 3e4;
311
+ const ranLongEnough = Date.now() - supervised.startedAt > STABLE_RUNTIME_MS;
203
312
  const nextCrashes = ranLongEnough ? 1 : prevCrashes + 1;
204
313
  const backoff = Math.min(MAX_BACKOFF_MS, MIN_BACKOFF_MS * 2 ** Math.max(0, nextCrashes - 1));
205
314
  this.deps.log(
206
- `supervisor: ${agent.name} exited code=${code} signal=${signal ?? "none"} consecutive=${nextCrashes} \u2192 respawn in ${backoff}ms`
315
+ `supervisor: ${agent.name} bridge exited code=${code} signal=${signal ?? "none"} consecutive=${nextCrashes} \u2192 respawn in ${backoff}ms`
207
316
  );
208
317
  supervised.restartTimer = setTimeout(() => {
209
318
  if (this.children.get(agent.name) !== supervised) return;
@@ -222,73 +331,82 @@ var Supervisor = class {
222
331
  }
223
332
  };
224
333
 
334
+ // src/lib/troop-sync.ts
335
+ import { execFile as execFile2 } from "child_process";
336
+ import process2 from "process";
337
+ import { promisify as promisify2 } from "util";
338
+ var execFileAsync2 = promisify2(execFile2);
339
+ var TICK_MS = 5 * 60 * 1e3;
340
+ var TroopSync = class {
341
+ constructor(deps) {
342
+ this.deps = deps;
343
+ }
344
+ timer;
345
+ inflight = false;
346
+ start() {
347
+ if (this.timer) return;
348
+ setTimeout(() => this.tick(), 3e4).unref();
349
+ this.timer = setInterval(() => this.tick(), TICK_MS);
350
+ this.deps.log("troop-sync: loop started (interval=5min)");
351
+ }
352
+ stop() {
353
+ if (this.timer) clearInterval(this.timer);
354
+ this.timer = void 0;
355
+ }
356
+ async tick() {
357
+ if (this.inflight) return;
358
+ this.inflight = true;
359
+ try {
360
+ const agents = listAgents();
361
+ if (agents.length === 0) return;
362
+ this.deps.log(`troop-sync: reconciling ${agents.length} agent(s)`);
363
+ for (const agent of agents) {
364
+ await this.syncOne(agent.name);
365
+ }
366
+ } finally {
367
+ this.inflight = false;
368
+ }
369
+ }
370
+ async syncOne(name) {
371
+ try {
372
+ await execFileAsync2(
373
+ this.deps.apesBin,
374
+ ["run", "--as", name, "--wait", "--", "apes", "agents", "sync"],
375
+ { maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4 }
376
+ );
377
+ } catch (err) {
378
+ this.deps.log(`troop-sync: ${name} failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
379
+ }
380
+ }
381
+ };
382
+
225
383
  // src/index.ts
226
- var HOST = "127.0.0.1";
227
- var PORT = Number(process.env.OPENAPE_NEST_PORT ?? 9091);
228
- var APES_BIN = process.env.OPENAPE_APES_BIN ?? "apes";
384
+ var APES_BIN = process3.env.OPENAPE_APES_BIN ?? "apes";
229
385
  function log(line) {
230
- process.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
386
+ process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
231
387
  `);
232
388
  }
233
389
  var supervisor = new Supervisor({ apesBin: APES_BIN, log });
390
+ var troopSync = new TroopSync({ apesBin: APES_BIN, log });
391
+ var intentChannel = new IntentChannel({ apesBin: APES_BIN, supervisor, log });
234
392
  supervisor.reconcile(listAgents());
235
- log(`nest: supervisor reconciled, ${supervisor.size()} agent process(es) running`);
236
- async function readJsonBody(req) {
237
- const chunks = [];
238
- for await (const chunk of req) chunks.push(chunk);
239
- if (chunks.length === 0) return {};
240
- const text = Buffer.concat(chunks).toString("utf8");
241
- if (!text.trim()) return {};
242
- try {
243
- return JSON.parse(text);
244
- } catch {
245
- throw new Error("invalid JSON body");
246
- }
247
- }
248
- function send(res, status, body) {
249
- res.writeHead(status, { "content-type": "application/json" });
250
- res.end(JSON.stringify(body));
251
- }
252
- var server = createServer((req, res) => {
253
- ;
254
- (async () => {
255
- try {
256
- const url = new URL(req.url ?? "/", `http://${HOST}:${PORT}`);
257
- const body = req.method && ["POST", "PUT", "PATCH"].includes(req.method) ? await readJsonBody(req) : {};
258
- const ctx = { url, body, log, apesBin: APES_BIN, supervisor };
259
- if (req.method === "GET" && url.pathname === "/status") {
260
- return send(res, 200, handleNestStatus(ctx));
261
- }
262
- if (req.method === "GET" && url.pathname === "/agents") {
263
- return send(res, 200, handleAgentsList(ctx));
264
- }
265
- if (req.method === "POST" && url.pathname === "/agents") {
266
- const result = await handleAgentSpawn(ctx);
267
- return send(res, 201, result);
268
- }
269
- const destroyMatch = req.method === "DELETE" && url.pathname.match(/^\/agents\/([^/]+)$/);
270
- if (destroyMatch) {
271
- const result = await handleAgentDestroy(ctx, destroyMatch[1]);
272
- return send(res, 200, result);
273
- }
274
- send(res, 404, { error: "not found" });
275
- } catch (err) {
276
- const msg = err instanceof Error ? err.message : String(err);
277
- log(`nest: request failed: ${msg}`);
278
- send(res, 500, { error: msg });
279
- }
280
- })();
281
- });
282
- server.listen(PORT, HOST, () => {
283
- log(`nest: listening on http://${HOST}:${PORT}`);
284
- });
285
- process.on("SIGTERM", () => {
286
- log("nest: SIGTERM \u2014 stopping supervisor");
393
+ log(`nest: supervisor reconciled, ${supervisor.size()} bridge process(es) starting`);
394
+ troopSync.start();
395
+ intentChannel.start();
396
+ var reaperTimer = setInterval(reapStaleResponses, 60 * 60 * 1e3, log);
397
+ process3.on("SIGTERM", () => {
398
+ log("nest: SIGTERM \u2014 stopping");
287
399
  supervisor.stopAll();
288
- server.close(() => process.exit(0));
400
+ troopSync.stop();
401
+ intentChannel.stop();
402
+ clearInterval(reaperTimer);
403
+ process3.exit(0);
289
404
  });
290
- process.on("SIGINT", () => {
291
- log("nest: SIGINT \u2014 stopping supervisor");
405
+ process3.on("SIGINT", () => {
406
+ log("nest: SIGINT \u2014 stopping");
292
407
  supervisor.stopAll();
293
- server.close(() => process.exit(0));
408
+ troopSync.stop();
409
+ intentChannel.stop();
410
+ clearInterval(reaperTimer);
411
+ process3.exit(0);
294
412
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/nest",
3
- "version": "0.2.2",
3
+ "version": "1.0.0",
4
4
  "description": "OpenApe Nest — local control-plane daemon that supervises agent processes on this computer. Talks to troop SP for ownership state, spawns/destroys agents via DDISA always-grants, supervises chat-bridge children (replacing per-agent launchd plists).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "ofetch": "^1.4.1",
20
- "@openape/cli-auth": "0.3.0"
20
+ "@openape/core": "0.16.0",
21
+ "@openape/cli-auth": "0.4.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@antfu/eslint-config": "^7.6.1",