appostle-installer 0.0.14 → 0.0.15

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/dist/appostle.js CHANGED
@@ -1832,7 +1832,13 @@ function extractTimestamps(record) {
1832
1832
  // Fork lineage — preserved across resume so the in-memory ManagedAgent
1833
1833
  // can flow `parentAgentId` into snapshots that drive the tab Split icon.
1834
1834
  ...record.parentAgentId ? { parentAgentId: record.parentAgentId } : {},
1835
- ...record.forkedFromMessageUuid ? { forkedFromMessageUuid: record.forkedFromMessageUuid } : {}
1835
+ ...record.forkedFromMessageUuid ? { forkedFromMessageUuid: record.forkedFromMessageUuid } : {},
1836
+ // Multi-tenant ownership — closes the daemon-restart gap. Old records
1837
+ // lack these fields → ownerUserId stays undefined (→ null in
1838
+ // registerSession), agent rehydrates as unscoped.
1839
+ ownerUserId: record.ownerUserId ?? null,
1840
+ sharedWithUserIds: record.sharedWithUserIds ?? [],
1841
+ ownerUsername: record.ownerUsername ?? null
1836
1842
  };
1837
1843
  }
1838
1844
  function hasRegisteredProvider(registeredProviders, value) {
@@ -1916,6 +1922,11 @@ function toAgentPayload(agent, options) {
1916
1922
  title: options?.title ?? null,
1917
1923
  labels: agent.labels,
1918
1924
  internal: agent.internal,
1925
+ // Surface ownership so the client can render an owner badge / detect
1926
+ // "shared with me" agents. `sharedWithUserIds` deliberately stays off
1927
+ // the snapshot — only owners read the full ACL, via the dedicated
1928
+ // `list_agent_shared_users_request` RPC.
1929
+ ownerUserId: agent.ownerUserId,
1919
1930
  // Fork lineage — the client's tab descriptor uses `parentAgentId` to
1920
1931
  // mark the agent as a fork (Split glyph). Carry it from the live
1921
1932
  // ManagedAgent so the marker doesn't disappear once the agent is
@@ -3982,7 +3993,19 @@ var AgentSnapshotPayloadSchema = z11.object({
3982
3993
  * lists at display time. The agent itself is a real, full-featured session
3983
3994
  * in every other respect; `internal` is a UI visibility hint, nothing more.
3984
3995
  */
3985
- internal: z11.boolean().optional()
3996
+ internal: z11.boolean().optional(),
3997
+ /**
3998
+ * Multi-tenant ownership (Phase 2c/4). Surfaces the auth-server user-id
3999
+ * of the agent's creator so the app can render an owner badge and
4000
+ * detect "shared with me" state (owner !== current user). Optional/
4001
+ * nullable for legacy agents created before per-agent ownership
4002
+ * existed — those render without a badge.
4003
+ *
4004
+ * NOTE: `sharedWithUserIds` is intentionally NOT exposed here. Only the
4005
+ * owner can enumerate the full ACL, via `list_agent_shared_users_request`.
4006
+ * The snapshot stays cheap and recipient-safe.
4007
+ */
4008
+ ownerUserId: z11.string().nullable().optional()
3986
4009
  });
3987
4010
  var VoiceAudioChunkMessageSchema = z11.object({
3988
4011
  type: z11.literal("voice_audio_chunk"),
@@ -5191,6 +5214,74 @@ var DeleteSessionUploadResponseSchema = z11.object({
5191
5214
  })
5192
5215
  ])
5193
5216
  });
5217
+ var ShareAgentWithUserRequestSchema = z11.object({
5218
+ type: z11.literal("share_agent_with_user_request"),
5219
+ requestId: z11.string(),
5220
+ /** Accepts full ID, unique prefix, or exact full title (server resolves). */
5221
+ agentId: z11.string(),
5222
+ /** Auth-server user-id of the recipient. */
5223
+ userId: z11.string()
5224
+ });
5225
+ var ShareAgentWithUserResponseSchema = z11.object({
5226
+ type: z11.literal("share_agent_with_user_response"),
5227
+ payload: z11.discriminatedUnion("ok", [
5228
+ z11.object({
5229
+ requestId: z11.string(),
5230
+ ok: z11.literal(true),
5231
+ /** Updated ACL after the share applied (owner is not included). */
5232
+ sharedWithUserIds: z11.array(z11.string())
5233
+ }),
5234
+ z11.object({
5235
+ requestId: z11.string(),
5236
+ ok: z11.literal(false),
5237
+ error: z11.string()
5238
+ })
5239
+ ])
5240
+ });
5241
+ var UnshareAgentWithUserRequestSchema = z11.object({
5242
+ type: z11.literal("unshare_agent_with_user_request"),
5243
+ requestId: z11.string(),
5244
+ agentId: z11.string(),
5245
+ userId: z11.string()
5246
+ });
5247
+ var UnshareAgentWithUserResponseSchema = z11.object({
5248
+ type: z11.literal("unshare_agent_with_user_response"),
5249
+ payload: z11.discriminatedUnion("ok", [
5250
+ z11.object({
5251
+ requestId: z11.string(),
5252
+ ok: z11.literal(true),
5253
+ sharedWithUserIds: z11.array(z11.string())
5254
+ }),
5255
+ z11.object({
5256
+ requestId: z11.string(),
5257
+ ok: z11.literal(false),
5258
+ error: z11.string()
5259
+ })
5260
+ ])
5261
+ });
5262
+ var ListAgentSharedUsersRequestSchema = z11.object({
5263
+ type: z11.literal("list_agent_shared_users_request"),
5264
+ requestId: z11.string(),
5265
+ agentId: z11.string()
5266
+ });
5267
+ var ListAgentSharedUsersResponseSchema = z11.object({
5268
+ type: z11.literal("list_agent_shared_users_response"),
5269
+ payload: z11.discriminatedUnion("ok", [
5270
+ z11.object({
5271
+ requestId: z11.string(),
5272
+ ok: z11.literal(true),
5273
+ /** Auth-server user-id of the owner (always present when ok). */
5274
+ ownerUserId: z11.string().nullable(),
5275
+ /** Auth-server user-ids that the owner has granted access to. */
5276
+ sharedWithUserIds: z11.array(z11.string())
5277
+ }),
5278
+ z11.object({
5279
+ requestId: z11.string(),
5280
+ ok: z11.literal(false),
5281
+ error: z11.string()
5282
+ })
5283
+ ])
5284
+ });
5194
5285
  var SessionImageSchema = z11.object({
5195
5286
  id: z11.string(),
5196
5287
  fileName: z11.string(),
@@ -5433,6 +5524,9 @@ var SessionInboundMessageSchema = z11.discriminatedUnion("type", [
5433
5524
  DeleteSessionUploadRequestSchema,
5434
5525
  ListSessionImagesRequestSchema,
5435
5526
  DeleteSessionImageRequestSchema,
5527
+ ShareAgentWithUserRequestSchema,
5528
+ UnshareAgentWithUserRequestSchema,
5529
+ ListAgentSharedUsersRequestSchema,
5436
5530
  FetchAttachmentBytesRequestSchema
5437
5531
  ]);
5438
5532
  var ActivityLogPayloadSchema = z11.object({
@@ -7016,6 +7110,9 @@ var SessionOutboundMessageSchema = z11.discriminatedUnion("type", [
7016
7110
  DeleteSessionUploadResponseSchema,
7017
7111
  ListSessionImagesResponseSchema,
7018
7112
  DeleteSessionImageResponseSchema,
7113
+ ShareAgentWithUserResponseSchema,
7114
+ UnshareAgentWithUserResponseSchema,
7115
+ ListAgentSharedUsersResponseSchema,
7019
7116
  FetchAttachmentBytesResponseSchema
7020
7117
  ]);
7021
7118
  var WSPingMessageSchema = z11.object({
@@ -7209,7 +7306,7 @@ import { exec } from "node:child_process";
7209
7306
  import { promisify as promisify3 } from "util";
7210
7307
  import { join as join14, resolve as resolve9, sep as sep2 } from "path";
7211
7308
  import { homedir as homedir5, hostname as osHostname } from "node:os";
7212
- import { z as z36 } from "zod";
7309
+ import { z as z38 } from "zod";
7213
7310
 
7214
7311
  // ../server/src/server/persisted-config.ts
7215
7312
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -7723,7 +7820,9 @@ function ensurePrng() {
7723
7820
  const cryptoObj = globalThis.crypto;
7724
7821
  if (cryptoObj?.getRandomValues) {
7725
7822
  nacl.setPRNG((x, n) => {
7726
- cryptoObj.getRandomValues(x.subarray(0, n));
7823
+ const buf = new Uint8Array(n);
7824
+ cryptoObj.getRandomValues(buf);
7825
+ x.set(buf, 0);
7727
7826
  });
7728
7827
  prngReady = true;
7729
7828
  return;
@@ -7834,8 +7933,8 @@ function base64ToArrayBuffer(base64) {
7834
7933
  // ../relay/dist/encrypted-channel.js
7835
7934
  var HANDSHAKE_RETRY_MS = 1e3;
7836
7935
  var MAX_PENDING_SENDS = 200;
7837
- async function createClientChannel(transport, daemonPublicKeyB64, events = {}) {
7838
- const keyPair = generateKeyPair();
7936
+ async function createClientChannel(transport, daemonPublicKeyB64, events = {}, staticKeyPair) {
7937
+ const keyPair = staticKeyPair ?? generateKeyPair();
7839
7938
  const daemonPublicKey = importPublicKey(daemonPublicKeyB64);
7840
7939
  const sharedKey = deriveSharedKey(keyPair.secretKey, daemonPublicKey);
7841
7940
  const channel = new EncryptedChannel(transport, sharedKey, events);
@@ -8007,6 +8106,16 @@ var EncryptedChannel = class {
8007
8106
  isOpen() {
8008
8107
  return this.state === "open";
8009
8108
  }
8109
+ /**
8110
+ * Peer's X25519 public key (base64) captured during the daemon-side
8111
+ * handshake. Returns `null` on the client side or when the channel was
8112
+ * built without this metadata (legacy code paths). Used by the daemon's
8113
+ * WS-server to resolve the connecting device → owning user.
8114
+ */
8115
+ getPeerPublicKeyB64() {
8116
+ const v = this.options.peerPublicKeyB64;
8117
+ return v && v.length > 0 ? v : null;
8118
+ }
8010
8119
  onTransitionToOpen(cb) {
8011
8120
  this.onOpenCallbacks.push(cb);
8012
8121
  }
@@ -9949,7 +10058,14 @@ async function ensureAgentLoaded(agentId, deps) {
9949
10058
  if (!config) {
9950
10059
  throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
9951
10060
  }
9952
- snapshot = await deps.agentManager.createAgent(config, agentId, { labels: record.labels });
10061
+ snapshot = await deps.agentManager.createAgent(config, agentId, {
10062
+ labels: record.labels,
10063
+ // Preserve multi-tenant ownership across the no-handle rehydrate
10064
+ // path (records that never landed a persistence handle, e.g. very
10065
+ // early agents). Without this, agents would silently drop their
10066
+ // owner whenever they took this branch through `ensureAgentLoaded`.
10067
+ ownerUserId: record.ownerUserId ?? null
10068
+ });
9953
10069
  deps.logger.info({ agentId, provider: record.provider }, "Agent created from stored config");
9954
10070
  }
9955
10071
  await deps.agentManager.hydrateTimelineFromProvider(agentId);
@@ -31261,6 +31377,53 @@ function buildProviderRegistry(logger, options) {
31261
31377
  );
31262
31378
  }
31263
31379
 
31380
+ // ../server/src/server/claude-profile.ts
31381
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync, rmSync as rmSync2 } from "node:fs";
31382
+ import path13 from "node:path";
31383
+ import os5 from "node:os";
31384
+ var SHARED_ITEMS = ["settings.json", "hooks", "agents", "skills", "plugins", "keybindings.json"];
31385
+ function getClaudeProfileDir(username) {
31386
+ return path13.join(os5.homedir(), `.claude-${username}`);
31387
+ }
31388
+ function ensureClaudeProfile(username, logger) {
31389
+ const profileDir = getClaudeProfileDir(username);
31390
+ const ownerDir = path13.join(os5.homedir(), ".claude");
31391
+ if (!existsSync10(ownerDir)) {
31392
+ throw new Error(`Owner claude config dir not found: ${ownerDir}`);
31393
+ }
31394
+ if (!existsSync10(profileDir)) {
31395
+ mkdirSync5(profileDir, { recursive: true });
31396
+ logger?.info({ profileDir, username }, "created claude profile directory");
31397
+ }
31398
+ for (const item of SHARED_ITEMS) {
31399
+ const target = path13.join(ownerDir, item);
31400
+ const link = path13.join(profileDir, item);
31401
+ if (!existsSync10(target)) continue;
31402
+ if (existsSync10(link)) continue;
31403
+ symlinkSync(target, link);
31404
+ logger?.info({ item, profileDir }, "symlinked shared config item");
31405
+ }
31406
+ return profileDir;
31407
+ }
31408
+ function hasClaudeAuth(username) {
31409
+ const profileDir = getClaudeProfileDir(username);
31410
+ return existsSync10(profileDir);
31411
+ }
31412
+ function removeClaudeProfile(username, logger) {
31413
+ const profileDir = getClaudeProfileDir(username);
31414
+ if (!existsSync10(profileDir)) return;
31415
+ rmSync2(profileDir, { recursive: true, force: true });
31416
+ logger?.info({ profileDir, username }, "removed claude profile directory");
31417
+ }
31418
+
31419
+ // ../server/src/server/agent/agent-manager.ts
31420
+ import { z as z33 } from "zod";
31421
+ import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
31422
+
31423
+ // ../server/src/server/agent/handoff-mcp.ts
31424
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
31425
+ import { z as z32 } from "zod";
31426
+
31264
31427
  // ../server/src/server/agent/agent-metadata-generator.ts
31265
31428
  import { basename as basename5 } from "path";
31266
31429
  import { z as z31 } from "zod";
@@ -31738,6 +31901,14 @@ function scheduleAgentMetadataGeneration(options) {
31738
31901
  });
31739
31902
  }
31740
31903
 
31904
+ // ../server/src/server/agent/agent-manager.ts
31905
+ var AgentIdSchema = z33.string().uuid();
31906
+ function canUserAccessAgent(agent, requesterUserId) {
31907
+ if (agent.ownerUserId === null) return true;
31908
+ if (agent.ownerUserId === requesterUserId) return true;
31909
+ return agent.sharedWithUserIds.includes(requesterUserId);
31910
+ }
31911
+
31741
31912
  // ../server/src/server/agent/timeline-append.ts
31742
31913
  async function appendTimelineItemIfAgentKnown(options) {
31743
31914
  try {
@@ -32121,25 +32292,25 @@ async function buildProjectPlacementForCwd(input) {
32121
32292
  }
32122
32293
 
32123
32294
  // ../server/src/server/workspace-registry.ts
32124
- import { z as z32 } from "zod";
32125
- var PersistedProjectRecordSchema = z32.object({
32126
- projectId: z32.string(),
32127
- rootPath: z32.string(),
32128
- kind: z32.enum(["git", "non_git"]),
32129
- displayName: z32.string(),
32130
- createdAt: z32.string(),
32131
- updatedAt: z32.string(),
32132
- archivedAt: z32.string().nullable()
32133
- });
32134
- var PersistedWorkspaceRecordSchema = z32.object({
32135
- workspaceId: z32.string(),
32136
- projectId: z32.string(),
32137
- cwd: z32.string(),
32138
- kind: z32.enum(["local_checkout", "worktree", "directory"]),
32139
- displayName: z32.string(),
32140
- createdAt: z32.string(),
32141
- updatedAt: z32.string(),
32142
- archivedAt: z32.string().nullable()
32295
+ import { z as z34 } from "zod";
32296
+ var PersistedProjectRecordSchema = z34.object({
32297
+ projectId: z34.string(),
32298
+ rootPath: z34.string(),
32299
+ kind: z34.enum(["git", "non_git"]),
32300
+ displayName: z34.string(),
32301
+ createdAt: z34.string(),
32302
+ updatedAt: z34.string(),
32303
+ archivedAt: z34.string().nullable()
32304
+ });
32305
+ var PersistedWorkspaceRecordSchema = z34.object({
32306
+ workspaceId: z34.string(),
32307
+ projectId: z34.string(),
32308
+ cwd: z34.string(),
32309
+ kind: z34.enum(["local_checkout", "worktree", "directory"]),
32310
+ displayName: z34.string(),
32311
+ createdAt: z34.string(),
32312
+ updatedAt: z34.string(),
32313
+ archivedAt: z34.string().nullable()
32143
32314
  });
32144
32315
  function createPersistedProjectRecord(input) {
32145
32316
  return PersistedProjectRecordSchema.parse({
@@ -32218,7 +32389,7 @@ function isVoicePermissionAllowed(request) {
32218
32389
 
32219
32390
  // ../server/src/server/file-explorer/service.ts
32220
32391
  import { promises as fs8 } from "fs";
32221
- import path13 from "path";
32392
+ import path14 from "path";
32222
32393
 
32223
32394
  // ../server/src/server/path-utils.ts
32224
32395
  import { homedir as homedir2 } from "node:os";
@@ -32272,7 +32443,7 @@ async function listDirectoryEntries({
32272
32443
  const dirents = await fs8.readdir(directoryPath, { withFileTypes: true });
32273
32444
  const entriesWithNulls = await Promise.all(
32274
32445
  dirents.map(async (dirent) => {
32275
- const targetPath = path13.join(directoryPath, dirent.name);
32446
+ const targetPath = path14.join(directoryPath, dirent.name);
32276
32447
  const kind = dirent.isDirectory() ? "directory" : "file";
32277
32448
  try {
32278
32449
  return await buildEntryPayload({
@@ -32311,7 +32482,7 @@ async function readExplorerFile({
32311
32482
  if (!stats.isFile()) {
32312
32483
  throw new Error("Requested path is not a file");
32313
32484
  }
32314
- const ext = path13.extname(filePath).toLowerCase();
32485
+ const ext = path14.extname(filePath).toLowerCase();
32315
32486
  const basePayload = {
32316
32487
  path: normalizeRelativePath({ root, targetPath: filePath }),
32317
32488
  size: stats.size,
@@ -32349,7 +32520,7 @@ async function writeTextFile({
32349
32520
  relativePath,
32350
32521
  content
32351
32522
  }) {
32352
- const ext = path13.extname(relativePath).toLowerCase();
32523
+ const ext = path14.extname(relativePath).toLowerCase();
32353
32524
  if (ext in IMAGE_MIME_TYPES) {
32354
32525
  throw new Error(`Refusing to write '${relativePath}': binary/image file`);
32355
32526
  }
@@ -32359,7 +32530,7 @@ async function writeTextFile({
32359
32530
  await fs8.rename(tempPath, filePath);
32360
32531
  }
32361
32532
  async function deleteFile({ root, relativePath }) {
32362
- const ext = path13.extname(relativePath).toLowerCase();
32533
+ const ext = path14.extname(relativePath).toLowerCase();
32363
32534
  if (ext !== ".md") {
32364
32535
  throw new Error(`Refusing to delete '${relativePath}': only .md files allowed`);
32365
32536
  }
@@ -32393,7 +32564,7 @@ async function getDownloadableFileInfo({ root, relativePath }) {
32393
32564
  if (!stats.isFile()) {
32394
32565
  throw new Error("Requested path is not a file");
32395
32566
  }
32396
- const ext = path13.extname(filePath).toLowerCase();
32567
+ const ext = path14.extname(filePath).toLowerCase();
32397
32568
  let mimeType = "application/octet-stream";
32398
32569
  if (ext in IMAGE_MIME_TYPES) {
32399
32570
  mimeType = IMAGE_MIME_TYPES[ext] ?? mimeType;
@@ -32415,23 +32586,23 @@ async function getDownloadableFileInfo({ root, relativePath }) {
32415
32586
  return {
32416
32587
  path: normalizeRelativePath({ root, targetPath: filePath }),
32417
32588
  absolutePath: filePath,
32418
- fileName: path13.basename(filePath),
32589
+ fileName: path14.basename(filePath),
32419
32590
  mimeType,
32420
32591
  size: stats.size
32421
32592
  };
32422
32593
  }
32423
32594
  async function resolveScopedPath({ root, relativePath = "." }) {
32424
- const normalizedRoot = path13.resolve(root);
32595
+ const normalizedRoot = path14.resolve(root);
32425
32596
  const requestedPath = resolvePathFromBase(normalizedRoot, relativePath);
32426
- const relative = path13.relative(normalizedRoot, requestedPath);
32427
- if (relative !== "" && (relative.startsWith("..") || path13.isAbsolute(relative))) {
32597
+ const relative = path14.relative(normalizedRoot, requestedPath);
32598
+ if (relative !== "" && (relative.startsWith("..") || path14.isAbsolute(relative))) {
32428
32599
  throw new Error("Access outside of workspace is not allowed");
32429
32600
  }
32430
32601
  const realRoot = await fs8.realpath(normalizedRoot);
32431
32602
  try {
32432
32603
  const realPath = await fs8.realpath(requestedPath);
32433
- const realRelative = path13.relative(realRoot, realPath);
32434
- if (realRelative !== "" && (realRelative.startsWith("..") || path13.isAbsolute(realRelative))) {
32604
+ const realRelative = path14.relative(realRoot, realPath);
32605
+ if (realRelative !== "" && (realRelative.startsWith("..") || path14.isAbsolute(realRelative))) {
32435
32606
  throw new Error("Access outside of workspace is not allowed");
32436
32607
  }
32437
32608
  return requestedPath;
@@ -32462,10 +32633,10 @@ function isMissingEntryError(error) {
32462
32633
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
32463
32634
  }
32464
32635
  function normalizeRelativePath({ root, targetPath }) {
32465
- const normalizedRoot = path13.resolve(root);
32466
- const normalizedTarget = path13.resolve(targetPath);
32467
- const relative = path13.relative(normalizedRoot, normalizedTarget);
32468
- return relative === "" ? "." : relative.split(path13.sep).join("/");
32636
+ const normalizedRoot = path14.resolve(root);
32637
+ const normalizedTarget = path14.resolve(targetPath);
32638
+ const relative = path14.relative(normalizedRoot, normalizedTarget);
32639
+ return relative === "" ? "." : relative.split(path14.sep).join("/");
32469
32640
  }
32470
32641
  function textMimeTypeForExtension(ext) {
32471
32642
  return TEXT_MIME_TYPES[ext] ?? DEFAULT_TEXT_MIME_TYPE;
@@ -32818,65 +32989,65 @@ async function getProjectIcon(projectDir) {
32818
32989
  }
32819
32990
 
32820
32991
  // ../server/src/utils/path.ts
32821
- import os5 from "os";
32992
+ import os6 from "os";
32822
32993
  function expandTilde(path29) {
32823
32994
  if (path29.startsWith("~/")) {
32824
- const homeDir3 = process.env.HOME || os5.homedir();
32995
+ const homeDir3 = process.env.HOME || os6.homedir();
32825
32996
  return path29.replace("~", homeDir3);
32826
32997
  }
32827
32998
  if (path29 === "~") {
32828
- return process.env.HOME || os5.homedir();
32999
+ return process.env.HOME || os6.homedir();
32829
33000
  }
32830
33001
  return path29;
32831
33002
  }
32832
33003
 
32833
33004
  // ../server/src/server/skills/scanner.ts
32834
33005
  import fs9 from "node:fs/promises";
32835
- import os6 from "node:os";
32836
- import path14 from "node:path";
33006
+ import os7 from "node:os";
33007
+ import path15 from "node:path";
32837
33008
  var NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/i;
32838
33009
  function homeDir() {
32839
- return process.env.HOME || os6.homedir();
33010
+ return process.env.HOME || os7.homedir();
32840
33011
  }
32841
33012
  function codexHomeDir() {
32842
- return process.env.CODEX_HOME || path14.join(homeDir(), ".codex");
33013
+ return process.env.CODEX_HOME || path15.join(homeDir(), ".codex");
32843
33014
  }
32844
33015
  function resolveScopeDir(provider, scope, workspaceRoot) {
32845
33016
  if (scope === "codex-prompts") {
32846
33017
  if (provider !== "codex") {
32847
33018
  throw new Error(`Scope "codex-prompts" is only valid for provider "codex"`);
32848
33019
  }
32849
- return path14.join(codexHomeDir(), "prompts");
33020
+ return path15.join(codexHomeDir(), "prompts");
32850
33021
  }
32851
33022
  if (scope === "project") {
32852
33023
  if (!workspaceRoot) {
32853
33024
  throw new Error(`workspaceRoot is required for scope "project"`);
32854
33025
  }
32855
33026
  const dotDir = provider === "claude" ? ".claude" : ".codex";
32856
- return path14.join(workspaceRoot, dotDir, "skills");
33027
+ return path15.join(workspaceRoot, dotDir, "skills");
32857
33028
  }
32858
33029
  if (provider === "claude") {
32859
- return path14.join(homeDir(), ".claude", "skills");
33030
+ return path15.join(homeDir(), ".claude", "skills");
32860
33031
  }
32861
- return path14.join(codexHomeDir(), "skills");
33032
+ return path15.join(codexHomeDir(), "skills");
32862
33033
  }
32863
33034
  function allowedRoots(workspaceRoot) {
32864
33035
  const roots = [
32865
- path14.join(homeDir(), ".claude", "skills"),
32866
- path14.join(codexHomeDir(), "skills"),
32867
- path14.join(codexHomeDir(), "prompts")
33036
+ path15.join(homeDir(), ".claude", "skills"),
33037
+ path15.join(codexHomeDir(), "skills"),
33038
+ path15.join(codexHomeDir(), "prompts")
32868
33039
  ];
32869
33040
  if (workspaceRoot) {
32870
- roots.push(path14.join(workspaceRoot, ".claude", "skills"));
32871
- roots.push(path14.join(workspaceRoot, ".codex", "skills"));
33041
+ roots.push(path15.join(workspaceRoot, ".claude", "skills"));
33042
+ roots.push(path15.join(workspaceRoot, ".codex", "skills"));
32872
33043
  }
32873
- return roots.map((r) => path14.resolve(r));
33044
+ return roots.map((r) => path15.resolve(r));
32874
33045
  }
32875
33046
  function isInsideAllowedRoot(absPath, workspaceRoot) {
32876
- const resolved = path14.resolve(absPath);
33047
+ const resolved = path15.resolve(absPath);
32877
33048
  for (const root of allowedRoots(workspaceRoot)) {
32878
- const rel = path14.relative(root, resolved);
32879
- if (rel === "" || !rel.startsWith("..") && !path14.isAbsolute(rel)) {
33049
+ const rel = path15.relative(root, resolved);
33050
+ if (rel === "" || !rel.startsWith("..") && !path15.isAbsolute(rel)) {
32880
33051
  return true;
32881
33052
  }
32882
33053
  }
@@ -33007,7 +33178,7 @@ async function listSkills(args) {
33007
33178
  if (!entry.name.endsWith(".md")) continue;
33008
33179
  const name = entry.name.slice(0, -".md".length);
33009
33180
  if (!name) continue;
33010
- const fullPath = path14.join(dir, entry.name);
33181
+ const fullPath = path15.join(dir, entry.name);
33011
33182
  const stat5 = await safeStat(fullPath);
33012
33183
  if (!stat5) continue;
33013
33184
  const description = await readDescriptionSafely(fullPath);
@@ -33025,8 +33196,8 @@ async function listSkills(args) {
33025
33196
  } else {
33026
33197
  for (const entry of entries) {
33027
33198
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
33028
- const skillDir = path14.join(dir, entry.name);
33029
- const skillPath = path14.join(skillDir, "SKILL.md");
33199
+ const skillDir = path15.join(dir, entry.name);
33200
+ const skillPath = path15.join(skillDir, "SKILL.md");
33030
33201
  const stat5 = await safeStat(skillPath);
33031
33202
  if (!stat5) continue;
33032
33203
  const description = await readDescriptionSafely(skillPath);
@@ -33073,7 +33244,7 @@ async function createSkill(args) {
33073
33244
  const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
33074
33245
  await fs9.mkdir(dir, { recursive: true });
33075
33246
  if (args.scope === "codex-prompts") {
33076
- const filePath2 = path14.join(dir, `${args.name}.md`);
33247
+ const filePath2 = path15.join(dir, `${args.name}.md`);
33077
33248
  try {
33078
33249
  await fs9.access(filePath2);
33079
33250
  throw new Error(`A prompt named "${args.name}" already exists at ${filePath2}`);
@@ -33089,7 +33260,7 @@ async function createSkill(args) {
33089
33260
  await fs9.writeFile(filePath2, initial2, "utf8");
33090
33261
  return { path: filePath2 };
33091
33262
  }
33092
- const skillDir = path14.join(dir, args.name);
33263
+ const skillDir = path15.join(dir, args.name);
33093
33264
  let dirExists = false;
33094
33265
  try {
33095
33266
  const stat5 = await fs9.stat(skillDir);
@@ -33101,7 +33272,7 @@ async function createSkill(args) {
33101
33272
  throw new Error(`A skill named "${args.name}" already exists at ${skillDir}`);
33102
33273
  }
33103
33274
  await fs9.mkdir(skillDir, { recursive: true });
33104
- const filePath = path14.join(skillDir, "SKILL.md");
33275
+ const filePath = path15.join(skillDir, "SKILL.md");
33105
33276
  const initial = buildStarterSkill(args.name);
33106
33277
  await fs9.writeFile(filePath, initial, "utf8");
33107
33278
  return { path: filePath };
@@ -33130,7 +33301,7 @@ Body of the prompt. Use \`$1\`, \`$2\`, ... or \`$ARGUMENTS\` for parameter expa
33130
33301
  `;
33131
33302
  }
33132
33303
  async function writeSkillFrontmatter(args, workspaceRoot) {
33133
- if (!path14.isAbsolute(args.path)) {
33304
+ if (!path15.isAbsolute(args.path)) {
33134
33305
  throw new Error(`writeSkillFrontmatter expects an absolute path; got "${args.path}"`);
33135
33306
  }
33136
33307
  if (!isInsideAllowedRoot(args.path, workspaceRoot)) {
@@ -33156,7 +33327,7 @@ ${original}`;
33156
33327
 
33157
33328
  // ../server/src/utils/directory-suggestions.ts
33158
33329
  import { readdir as readdir2, realpath, stat as stat3 } from "node:fs/promises";
33159
- import path15 from "node:path";
33330
+ import path16 from "node:path";
33160
33331
  var DEFAULT_LIMIT = 30;
33161
33332
  var MAX_LIMIT = 100;
33162
33333
  var DEFAULT_MAX_DEPTH = 6;
@@ -33242,7 +33413,7 @@ function normalizeLimit(limit) {
33242
33413
  return Math.max(1, Math.min(MAX_LIMIT, bounded));
33243
33414
  }
33244
33415
  async function searchWithinParentDirectory(input) {
33245
- const parentPath = path15.resolve(input.homeRoot, input.parentPart || ".");
33416
+ const parentPath = path16.resolve(input.homeRoot, input.parentPart || ".");
33246
33417
  const parentRoot = await resolveDirectory(parentPath);
33247
33418
  if (!parentRoot || !isPathInsideRoot(input.homeRoot, parentRoot)) {
33248
33419
  return [];
@@ -33307,7 +33478,7 @@ async function searchAcrossHomeTree(input) {
33307
33478
  return dedupeAndSort(ranked).slice(0, input.limit);
33308
33479
  }
33309
33480
  async function searchWorkspaceWithinParentDirectory(input) {
33310
- const parentPath = path15.resolve(input.workspaceRoot, input.parentPart || ".");
33481
+ const parentPath = path16.resolve(input.workspaceRoot, input.parentPart || ".");
33311
33482
  const parentRoot = await resolveDirectory(parentPath);
33312
33483
  if (!parentRoot || !isPathInsideRoot(input.workspaceRoot, parentRoot)) {
33313
33484
  return [];
@@ -33553,15 +33724,15 @@ function findSegmentMatchIndex(segments, predicate) {
33553
33724
  return -1;
33554
33725
  }
33555
33726
  function normalizeRelativePath2(homeRoot, absolutePath) {
33556
- const relative = path15.relative(homeRoot, absolutePath);
33727
+ const relative = path16.relative(homeRoot, absolutePath);
33557
33728
  if (!relative) {
33558
33729
  return ".";
33559
33730
  }
33560
- return relative.split(path15.sep).join("/");
33731
+ return relative.split(path16.sep).join("/");
33561
33732
  }
33562
33733
  function isPathInsideRoot(root, target) {
33563
- const relative = path15.relative(root, target);
33564
- return relative === "" || !relative.startsWith("..") && !path15.isAbsolute(relative);
33734
+ const relative = path16.relative(root, target);
33735
+ return relative === "" || !relative.startsWith("..") && !path16.isAbsolute(relative);
33565
33736
  }
33566
33737
  function normalizeQueryParts(query2, homeRoot) {
33567
33738
  const typedQuery = query2.trim().replace(/\\/g, "/");
@@ -33577,9 +33748,9 @@ function normalizeQueryParts(query2, homeRoot) {
33577
33748
  normalized = normalized.slice(1);
33578
33749
  }
33579
33750
  }
33580
- if (path15.isAbsolute(normalized)) {
33751
+ if (path16.isAbsolute(normalized)) {
33581
33752
  isRooted = true;
33582
- const absolute = path15.resolve(normalized);
33753
+ const absolute = path16.resolve(normalized);
33583
33754
  if (!isPathInsideRoot(homeRoot, absolute)) {
33584
33755
  return null;
33585
33756
  }
@@ -33618,8 +33789,8 @@ function normalizeQueryParts(query2, homeRoot) {
33618
33789
  }
33619
33790
  function normalizeWorkspaceQueryParts(query2, workspaceRoot) {
33620
33791
  let normalized = query2.trim().replace(/\\/g, "/");
33621
- if (path15.isAbsolute(normalized)) {
33622
- const absolute = path15.resolve(normalized);
33792
+ if (path16.isAbsolute(normalized)) {
33793
+ const absolute = path16.resolve(normalized);
33623
33794
  if (!isPathInsideRoot(workspaceRoot, absolute)) {
33624
33795
  return null;
33625
33796
  }
@@ -33645,7 +33816,7 @@ function normalizeWorkspaceQueryParts(query2, workspaceRoot) {
33645
33816
  }
33646
33817
  async function resolveDirectory(inputPath) {
33647
33818
  try {
33648
- const resolved = await realpath(path15.resolve(inputPath));
33819
+ const resolved = await realpath(path16.resolve(inputPath));
33649
33820
  const stats = await stat3(resolved);
33650
33821
  if (!stats.isDirectory()) {
33651
33822
  return null;
@@ -33672,7 +33843,7 @@ async function listChildDirectories(input) {
33672
33843
  if (!dirent.isDirectory() && !dirent.isSymbolicLink()) {
33673
33844
  continue;
33674
33845
  }
33675
- const candidatePath = path15.join(input.directory, dirent.name);
33846
+ const candidatePath = path16.join(input.directory, dirent.name);
33676
33847
  const absolutePath = await resolveDirectoryCandidate({
33677
33848
  candidatePath,
33678
33849
  dirent,
@@ -33709,7 +33880,7 @@ async function listWorkspaceChildEntries(input) {
33709
33880
  if (isIgnoredWorkspaceDirectoryName(dirent.name)) {
33710
33881
  continue;
33711
33882
  }
33712
- const candidatePath = path15.join(input.directory, dirent.name);
33883
+ const candidatePath = path16.join(input.directory, dirent.name);
33713
33884
  const entry = await resolveWorkspaceCandidate({
33714
33885
  candidatePath,
33715
33886
  dirent,
@@ -33732,7 +33903,7 @@ async function listWorkspaceChildEntries(input) {
33732
33903
  }
33733
33904
  async function resolveDirectoryCandidate(input) {
33734
33905
  if (input.dirent.isDirectory()) {
33735
- const resolved2 = path15.resolve(input.candidatePath);
33906
+ const resolved2 = path16.resolve(input.candidatePath);
33736
33907
  return isPathInsideRoot(input.homeRoot, resolved2) ? resolved2 : null;
33737
33908
  }
33738
33909
  const resolved = await resolveDirectory(input.candidatePath);
@@ -33743,14 +33914,14 @@ async function resolveDirectoryCandidate(input) {
33743
33914
  }
33744
33915
  async function resolveWorkspaceCandidate(input) {
33745
33916
  if (input.dirent.isDirectory()) {
33746
- const resolved = path15.resolve(input.candidatePath);
33917
+ const resolved = path16.resolve(input.candidatePath);
33747
33918
  if (!isPathInsideRoot(input.workspaceRoot, resolved)) {
33748
33919
  return null;
33749
33920
  }
33750
33921
  return { absolutePath: resolved, kind: "directory" };
33751
33922
  }
33752
33923
  if (input.dirent.isFile()) {
33753
- const resolved = path15.resolve(input.candidatePath);
33924
+ const resolved = path16.resolve(input.candidatePath);
33754
33925
  if (!isPathInsideRoot(input.workspaceRoot, resolved)) {
33755
33926
  return null;
33756
33927
  }
@@ -33830,7 +34001,7 @@ function pruneWorkspaceEntryListCache() {
33830
34001
  // ../server/src/utils/directory-listing.ts
33831
34002
  import { readdir as readdir3, stat as stat4, realpath as realpath2 } from "node:fs/promises";
33832
34003
  import { homedir as homedir3 } from "node:os";
33833
- import path16 from "node:path";
34004
+ import path17 from "node:path";
33834
34005
  var DEFAULT_LIMIT2 = 500;
33835
34006
  async function listDirectoryContents(options) {
33836
34007
  const includeFiles = options.includeFiles ?? false;
@@ -33841,7 +34012,7 @@ async function listDirectoryContents(options) {
33841
34012
  const collected = [];
33842
34013
  for (const dirent of dirents) {
33843
34014
  if (!includeHidden && dirent.name.startsWith(".")) continue;
33844
- const childPath = path16.join(resolvedPath, dirent.name);
34015
+ const childPath = path17.join(resolvedPath, dirent.name);
33845
34016
  const kind = await classifyEntry(dirent, childPath);
33846
34017
  if (!kind) continue;
33847
34018
  if (kind === "file" && !includeFiles) continue;
@@ -33849,7 +34020,7 @@ async function listDirectoryContents(options) {
33849
34020
  if (collected.length >= limit) break;
33850
34021
  }
33851
34022
  collected.sort(compareEntries);
33852
- const parent = path16.dirname(resolvedPath);
34023
+ const parent = path17.dirname(resolvedPath);
33853
34024
  return {
33854
34025
  path: resolvedPath,
33855
34026
  parent: parent === resolvedPath ? null : parent,
@@ -33860,12 +34031,12 @@ async function resolveAbsolutePath(rawPath) {
33860
34031
  const home = process.env.HOME ?? homedir3();
33861
34032
  const trimmed = rawPath.trim();
33862
34033
  if (trimmed === "" || trimmed === "~") {
33863
- return path16.resolve(home);
34034
+ return path17.resolve(home);
33864
34035
  }
33865
34036
  if (trimmed.startsWith("~/")) {
33866
- return path16.resolve(home, trimmed.slice(2));
34037
+ return path17.resolve(home, trimmed.slice(2));
33867
34038
  }
33868
- if (!path16.isAbsolute(trimmed)) {
34039
+ if (!path17.isAbsolute(trimmed)) {
33869
34040
  throw new Error(
33870
34041
  `list_directory requires an absolute path, an empty string, or a "~"-prefixed path; got ${JSON.stringify(rawPath)}`
33871
34042
  );
@@ -33873,7 +34044,7 @@ async function resolveAbsolutePath(rawPath) {
33873
34044
  try {
33874
34045
  return await realpath2(trimmed);
33875
34046
  } catch {
33876
- return path16.resolve(trimmed);
34047
+ return path17.resolve(trimmed);
33877
34048
  }
33878
34049
  }
33879
34050
  async function classifyEntry(dirent, fullPath) {
@@ -33913,10 +34084,10 @@ function resolveClientMessageId(clientMessageId, generateId = uuidv45) {
33913
34084
  }
33914
34085
 
33915
34086
  // ../server/src/server/chat/chat-service.ts
33916
- import { z as z33 } from "zod";
33917
- var ChatStorePayloadSchema = z33.object({
33918
- rooms: z33.array(ChatRoomSchema),
33919
- messages: z33.array(ChatMessageSchema)
34087
+ import { z as z35 } from "zod";
34088
+ var ChatStorePayloadSchema = z35.object({
34089
+ rooms: z35.array(ChatRoomSchema),
34090
+ messages: z35.array(ChatMessageSchema)
33920
34091
  });
33921
34092
  var ChatServiceError = class extends Error {
33922
34093
  constructor(code, message) {
@@ -34007,33 +34178,33 @@ function buildChatMentionNotification(input) {
34007
34178
 
34008
34179
  // ../server/src/server/roles/scanner.ts
34009
34180
  import fs10 from "node:fs/promises";
34010
- import os7 from "node:os";
34011
- import path17 from "node:path";
34181
+ import os8 from "node:os";
34182
+ import path18 from "node:path";
34012
34183
  var NAME_REGEX2 = /^[a-z0-9][a-z0-9._-]*$/i;
34013
34184
  function homeDir2() {
34014
- return process.env.HOME || os7.homedir();
34185
+ return process.env.HOME || os8.homedir();
34015
34186
  }
34016
34187
  function resolveScopeDir2(scope, workspaceRoot) {
34017
34188
  if (scope === "project") {
34018
34189
  if (!workspaceRoot) {
34019
34190
  throw new Error('workspaceRoot is required for scope "project"');
34020
34191
  }
34021
- return path17.join(workspaceRoot, ".roles");
34192
+ return path18.join(workspaceRoot, ".roles");
34022
34193
  }
34023
- return path17.join(homeDir2(), ".appostle", ".roles");
34194
+ return path18.join(homeDir2(), ".appostle", ".roles");
34024
34195
  }
34025
34196
  function allowedRoots2(workspaceRoot) {
34026
- const roots = [path17.join(homeDir2(), ".appostle", ".roles")];
34197
+ const roots = [path18.join(homeDir2(), ".appostle", ".roles")];
34027
34198
  if (workspaceRoot) {
34028
- roots.push(path17.join(workspaceRoot, ".roles"));
34199
+ roots.push(path18.join(workspaceRoot, ".roles"));
34029
34200
  }
34030
- return roots.map((r) => path17.resolve(r));
34201
+ return roots.map((r) => path18.resolve(r));
34031
34202
  }
34032
34203
  function isInsideAllowedRoot2(absPath, workspaceRoot) {
34033
- const resolved = path17.resolve(absPath);
34204
+ const resolved = path18.resolve(absPath);
34034
34205
  for (const root of allowedRoots2(workspaceRoot)) {
34035
- const rel = path17.relative(root, resolved);
34036
- if (rel === "" || !rel.startsWith("..") && !path17.isAbsolute(rel)) {
34206
+ const rel = path18.relative(root, resolved);
34207
+ if (rel === "" || !rel.startsWith("..") && !path18.isAbsolute(rel)) {
34037
34208
  return true;
34038
34209
  }
34039
34210
  }
@@ -34231,7 +34402,7 @@ async function readRolesFromDir(scope, dir, category) {
34231
34402
  if (!entry.name.endsWith(".md")) continue;
34232
34403
  const name = entry.name.slice(0, -".md".length);
34233
34404
  if (!name || !NAME_REGEX2.test(name)) continue;
34234
- const fullPath = path17.join(dir, entry.name);
34405
+ const fullPath = path18.join(dir, entry.name);
34235
34406
  let stat5;
34236
34407
  try {
34237
34408
  const s = await fs10.stat(fullPath);
@@ -34281,7 +34452,7 @@ async function readRolesFromScopeDir(scope, scopeDir) {
34281
34452
  }
34282
34453
  const flat = await readRolesFromDir(scope, scopeDir, null);
34283
34454
  const categoryResults = await Promise.all(
34284
- topEntries.filter((e) => e.isDirectory() && NAME_REGEX2.test(e.name)).map((e) => readRolesFromDir(scope, path17.join(scopeDir, e.name), e.name))
34455
+ topEntries.filter((e) => e.isDirectory() && NAME_REGEX2.test(e.name)).map((e) => readRolesFromDir(scope, path18.join(scopeDir, e.name), e.name))
34285
34456
  );
34286
34457
  const all = [...flat, ...categoryResults.flat()];
34287
34458
  all.sort((a, b) => {
@@ -34321,9 +34492,9 @@ async function createRole(args) {
34321
34492
  throw new Error(`Role name must not contain path separators or "..".`);
34322
34493
  }
34323
34494
  const scopeDir = resolveScopeDir2(args.scope, args.workspaceRoot);
34324
- const dir = args.category ? path17.join(scopeDir, args.category) : scopeDir;
34495
+ const dir = args.category ? path18.join(scopeDir, args.category) : scopeDir;
34325
34496
  await fs10.mkdir(dir, { recursive: true });
34326
- const filePath = path17.join(dir, `${args.name}.md`);
34497
+ const filePath = path18.join(dir, `${args.name}.md`);
34327
34498
  try {
34328
34499
  await fs10.access(filePath);
34329
34500
  throw new Error(`A role named "${args.name}" already exists at ${filePath}`);
@@ -34357,7 +34528,7 @@ the role is invoked.
34357
34528
  `;
34358
34529
  }
34359
34530
  async function writeRoleFrontmatter(args, workspaceRoot) {
34360
- if (!path17.isAbsolute(args.path)) {
34531
+ if (!path18.isAbsolute(args.path)) {
34361
34532
  throw new Error(`writeRoleFrontmatter expects an absolute path; got "${args.path}"`);
34362
34533
  }
34363
34534
  if (!isInsideAllowedRoot2(args.path, workspaceRoot)) {
@@ -34381,20 +34552,20 @@ ${original}`;
34381
34552
  await fs10.writeFile(args.path, nextContent, "utf8");
34382
34553
  }
34383
34554
  async function moveRole(args, workspaceRoot) {
34384
- if (!path17.isAbsolute(args.path)) {
34555
+ if (!path18.isAbsolute(args.path)) {
34385
34556
  throw new Error(`moveRole expects an absolute path; got "${args.path}"`);
34386
34557
  }
34387
34558
  if (!isInsideAllowedRoot2(args.path, workspaceRoot)) {
34388
34559
  throw new Error(`Path "${args.path}" is not inside an allowlisted role root`);
34389
34560
  }
34390
- const oldDir = path17.dirname(args.path);
34391
- const oldFilename = path17.basename(args.path, ".md");
34561
+ const oldDir = path18.dirname(args.path);
34562
+ const oldFilename = path18.basename(args.path, ".md");
34392
34563
  const roots = allowedRoots2(workspaceRoot);
34393
- const rolesRoot = roots.find((r) => path17.resolve(args.path).startsWith(r));
34564
+ const rolesRoot = roots.find((r) => path18.resolve(args.path).startsWith(r));
34394
34565
  if (!rolesRoot) {
34395
34566
  throw new Error(`Cannot determine roles root for "${args.path}"`);
34396
34567
  }
34397
- const relFromRoot = path17.relative(rolesRoot, path17.dirname(args.path));
34568
+ const relFromRoot = path18.relative(rolesRoot, path18.dirname(args.path));
34398
34569
  const currentCategory = relFromRoot && relFromRoot !== "." ? relFromRoot : "";
34399
34570
  const newName = args.newName ?? oldFilename;
34400
34571
  const newCategory = args.newCategory !== void 0 ? args.newCategory : currentCategory;
@@ -34404,9 +34575,9 @@ async function moveRole(args, workspaceRoot) {
34404
34575
  if (newCategory && !NAME_REGEX2.test(newCategory)) {
34405
34576
  throw new Error(`Invalid category name: "${newCategory}"`);
34406
34577
  }
34407
- const newDir = newCategory ? path17.join(rolesRoot, newCategory) : rolesRoot;
34408
- const newPath = path17.join(newDir, `${newName}.md`);
34409
- if (path17.resolve(newPath) === path17.resolve(args.path)) {
34578
+ const newDir = newCategory ? path18.join(rolesRoot, newCategory) : rolesRoot;
34579
+ const newPath = path18.join(newDir, `${newName}.md`);
34580
+ if (path18.resolve(newPath) === path18.resolve(args.path)) {
34410
34581
  return { path: args.path };
34411
34582
  }
34412
34583
  await fs10.mkdir(newDir, { recursive: true });
@@ -34444,17 +34615,17 @@ async function moveRole(args, workspaceRoot) {
34444
34615
 
34445
34616
  // ../server/src/server/brands/scanner.ts
34446
34617
  import fs11 from "node:fs/promises";
34447
- import path18 from "node:path";
34618
+ import path19 from "node:path";
34448
34619
  var CATEGORY_REGEX = /^[a-z0-9][a-z0-9_-]*$/i;
34449
34620
  function resolveAssetsDir(workspaceRoot, category) {
34450
- const base = path18.join(workspaceRoot, ".appostle", "brand", "assets");
34621
+ const base = path19.join(workspaceRoot, ".appostle", "brand", "assets");
34451
34622
  if (!category) return base;
34452
34623
  if (!CATEGORY_REGEX.test(category) || category.includes("..")) {
34453
34624
  throw new Error(
34454
34625
  `Invalid asset category "${category}". Use letters, digits, dot, underscore, dash.`
34455
34626
  );
34456
34627
  }
34457
- return path18.join(base, category);
34628
+ return path19.join(base, category);
34458
34629
  }
34459
34630
  function relativePathFor(category, fileName) {
34460
34631
  return category ? `assets/${category}/${fileName}` : `assets/${fileName}`;
@@ -34470,9 +34641,9 @@ async function removeSiblingExtensions(dir, targetName, keepFileName) {
34470
34641
  if (entry === keepFileName) continue;
34471
34642
  if (!entry.startsWith(`${targetName}.`)) continue;
34472
34643
  try {
34473
- const stat5 = await fs11.stat(path18.join(dir, entry));
34644
+ const stat5 = await fs11.stat(path19.join(dir, entry));
34474
34645
  if (!stat5.isFile()) continue;
34475
- await fs11.unlink(path18.join(dir, entry));
34646
+ await fs11.unlink(path19.join(dir, entry));
34476
34647
  } catch {
34477
34648
  }
34478
34649
  }
@@ -34483,7 +34654,7 @@ function convertSvgToMono(svg, color) {
34483
34654
  async function runDerivations(options) {
34484
34655
  const { primaryAbsolutePath, assetsDir, category, derive } = options;
34485
34656
  if (derive.length === 0) return [];
34486
- const ext = path18.extname(primaryAbsolutePath).toLowerCase();
34657
+ const ext = path19.extname(primaryAbsolutePath).toLowerCase();
34487
34658
  const isSvg = ext === ".svg";
34488
34659
  const results = [];
34489
34660
  for (const spec of derive) {
@@ -34518,9 +34689,9 @@ async function runDerivations(options) {
34518
34689
  const sourceText = await fs11.readFile(primaryAbsolutePath, "utf8");
34519
34690
  const monoText = convertSvgToMono(sourceText, spec.color);
34520
34691
  const fileName = `${spec.targetName}${ext}`;
34521
- const destAbs = path18.resolve(assetsDir, fileName);
34522
- const rel = path18.relative(path18.resolve(assetsDir), destAbs);
34523
- if (rel.startsWith("..") || path18.isAbsolute(rel)) {
34692
+ const destAbs = path19.resolve(assetsDir, fileName);
34693
+ const rel = path19.relative(path19.resolve(assetsDir), destAbs);
34694
+ if (rel.startsWith("..") || path19.isAbsolute(rel)) {
34524
34695
  results.push({
34525
34696
  targetName: spec.targetName,
34526
34697
  relativePath: "",
@@ -34554,22 +34725,22 @@ function resolveScopeDir3(scope, workspaceRoot) {
34554
34725
  if (!workspaceRoot) {
34555
34726
  throw new Error('workspaceRoot is required for scope "project"');
34556
34727
  }
34557
- return path18.join(workspaceRoot, ".appostle", "brand");
34728
+ return path19.join(workspaceRoot, ".appostle", "brand");
34558
34729
  }
34559
34730
  throw new Error(`Unknown scope: ${scope}`);
34560
34731
  }
34561
34732
  function allowedRoots3(workspaceRoot) {
34562
34733
  const roots = [];
34563
34734
  if (workspaceRoot) {
34564
- roots.push(path18.join(workspaceRoot, ".appostle", "brand"));
34735
+ roots.push(path19.join(workspaceRoot, ".appostle", "brand"));
34565
34736
  }
34566
- return roots.map((r) => path18.resolve(r));
34737
+ return roots.map((r) => path19.resolve(r));
34567
34738
  }
34568
34739
  function isInsideAllowedRoot3(absPath, workspaceRoot) {
34569
- const resolved = path18.resolve(absPath);
34740
+ const resolved = path19.resolve(absPath);
34570
34741
  for (const root of allowedRoots3(workspaceRoot)) {
34571
- const rel = path18.relative(root, resolved);
34572
- if (rel === "" || !rel.startsWith("..") && !path18.isAbsolute(rel)) {
34742
+ const rel = path19.relative(root, resolved);
34743
+ if (rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel)) {
34573
34744
  return true;
34574
34745
  }
34575
34746
  }
@@ -34783,7 +34954,7 @@ async function readBrandsFromDir(scope, dir) {
34783
34954
  if (!entry.name.endsWith(".md")) continue;
34784
34955
  const name = entry.name.slice(0, -".md".length);
34785
34956
  if (!name || !NAME_REGEX3.test(name)) continue;
34786
- const fullPath = path18.join(dir, entry.name);
34957
+ const fullPath = path19.join(dir, entry.name);
34787
34958
  let stat5;
34788
34959
  try {
34789
34960
  const s = await fs11.stat(fullPath);
@@ -34827,7 +34998,7 @@ async function createBrand(args) {
34827
34998
  }
34828
34999
  const dir = resolveScopeDir3("project", args.workspaceRoot);
34829
35000
  await fs11.mkdir(dir, { recursive: true });
34830
- const filePath = path18.join(dir, `${args.name}.md`);
35001
+ const filePath = path19.join(dir, `${args.name}.md`);
34831
35002
  try {
34832
35003
  await fs11.access(filePath);
34833
35004
  throw new Error(`A brand named "${args.name}" already exists at ${filePath}`);
@@ -34883,7 +35054,7 @@ async function copyBrandAsset(args) {
34883
35054
  if (!args.workspaceRoot) {
34884
35055
  throw new Error("workspaceRoot is required to copy a brand asset");
34885
35056
  }
34886
- if (!args.sourcePath || !path18.isAbsolute(args.sourcePath)) {
35057
+ if (!args.sourcePath || !path19.isAbsolute(args.sourcePath)) {
34887
35058
  throw new Error(`copyBrandAsset expects an absolute sourcePath; got "${args.sourcePath}"`);
34888
35059
  }
34889
35060
  if (!TARGET_NAME_REGEX.test(args.targetName) || args.targetName.includes("..")) {
@@ -34895,12 +35066,12 @@ async function copyBrandAsset(args) {
34895
35066
  if (!stats.isFile()) {
34896
35067
  throw new Error(`Source path is not a regular file: ${args.sourcePath}`);
34897
35068
  }
34898
- const ext = path18.extname(args.sourcePath).toLowerCase();
35069
+ const ext = path19.extname(args.sourcePath).toLowerCase();
34899
35070
  const fileName = `${args.targetName}${ext}`;
34900
35071
  const assetsDir = resolveAssetsDir(args.workspaceRoot, args.category);
34901
- const destAbs = path18.resolve(assetsDir, fileName);
34902
- const rel = path18.relative(path18.resolve(assetsDir), destAbs);
34903
- if (rel.startsWith("..") || path18.isAbsolute(rel)) {
35072
+ const destAbs = path19.resolve(assetsDir, fileName);
35073
+ const rel = path19.relative(path19.resolve(assetsDir), destAbs);
35074
+ if (rel.startsWith("..") || path19.isAbsolute(rel)) {
34904
35075
  throw new Error(`Refusing to write outside of .appostle/brand/assets: ${destAbs}`);
34905
35076
  }
34906
35077
  await fs11.mkdir(assetsDir, { recursive: true });
@@ -34930,13 +35101,13 @@ async function uploadBrandAsset(args) {
34930
35101
  if (!args.dataBase64 || args.dataBase64.trim().length === 0) {
34931
35102
  throw new Error("No file data provided for brand asset upload");
34932
35103
  }
34933
- const extFromSource = args.sourceName ? path18.extname(args.sourceName).toLowerCase() : "";
35104
+ const extFromSource = args.sourceName ? path19.extname(args.sourceName).toLowerCase() : "";
34934
35105
  const ext = extFromSource || ".png";
34935
35106
  const fileName = `${args.targetName}${ext}`;
34936
35107
  const assetsDir = resolveAssetsDir(args.workspaceRoot, args.category);
34937
- const destAbs = path18.resolve(assetsDir, fileName);
34938
- const rel = path18.relative(path18.resolve(assetsDir), destAbs);
34939
- if (rel.startsWith("..") || path18.isAbsolute(rel)) {
35108
+ const destAbs = path19.resolve(assetsDir, fileName);
35109
+ const rel = path19.relative(path19.resolve(assetsDir), destAbs);
35110
+ if (rel.startsWith("..") || path19.isAbsolute(rel)) {
34940
35111
  throw new Error(`Refusing to write outside of .appostle/brand/assets: ${destAbs}`);
34941
35112
  }
34942
35113
  let data;
@@ -34964,7 +35135,7 @@ async function uploadBrandAsset(args) {
34964
35135
  };
34965
35136
  }
34966
35137
  async function writeBrandFrontmatter(args, workspaceRoot) {
34967
- if (!path18.isAbsolute(args.path)) {
35138
+ if (!path19.isAbsolute(args.path)) {
34968
35139
  throw new Error(`writeBrandFrontmatter expects an absolute path; got "${args.path}"`);
34969
35140
  }
34970
35141
  if (!isInsideAllowedRoot3(args.path, workspaceRoot)) {
@@ -34989,10 +35160,10 @@ ${original}`;
34989
35160
  }
34990
35161
 
34991
35162
  // ../server/src/server/brand/token-generator.ts
34992
- import { z as z34 } from "zod";
35163
+ import { z as z36 } from "zod";
34993
35164
  var HEX6 = /^#[0-9a-f]{6}$/i;
34994
- var TokensResponseSchema = z34.object({
34995
- tokens: z34.record(z34.string(), z34.string())
35165
+ var TokensResponseSchema = z36.object({
35166
+ tokens: z36.record(z36.string(), z36.string())
34996
35167
  });
34997
35168
  function buildPrompt2(args) {
34998
35169
  const baseColours = args.paletteVars.filter((v) => v.type === "color");
@@ -35210,21 +35381,21 @@ async function generateAndApplyBrandTokens(options) {
35210
35381
 
35211
35382
  // ../server/src/server/brand/art-direction-generator.ts
35212
35383
  import { promises as fs12 } from "node:fs";
35213
- import path19 from "node:path";
35384
+ import path20 from "node:path";
35214
35385
  import { fileURLToPath as fileURLToPath2 } from "node:url";
35215
- import { z as z35 } from "zod";
35386
+ import { z as z37 } from "zod";
35216
35387
  var PROMPT_FILENAME = "art-direction-prompt.md";
35217
35388
  var MAX_LOOKUP_LEVELS = 10;
35218
35389
  async function findPromptFile() {
35219
- let dir = path19.dirname(fileURLToPath2(import.meta.url));
35390
+ let dir = path20.dirname(fileURLToPath2(import.meta.url));
35220
35391
  for (let i = 0; i < MAX_LOOKUP_LEVELS; i++) {
35221
- const candidate = path19.join(dir, PROMPT_FILENAME);
35392
+ const candidate = path20.join(dir, PROMPT_FILENAME);
35222
35393
  try {
35223
35394
  await fs12.access(candidate);
35224
35395
  return candidate;
35225
35396
  } catch {
35226
35397
  }
35227
- const parent = path19.dirname(dir);
35398
+ const parent = path20.dirname(dir);
35228
35399
  if (parent === dir) break;
35229
35400
  dir = parent;
35230
35401
  }
@@ -35257,8 +35428,8 @@ var EMBEDDED_PROMPT_FALLBACK = [
35257
35428
  "",
35258
35429
  'Return ONLY JSON: { "fields": { "art-direction.intent": "...", ... } }'
35259
35430
  ].join("\n");
35260
- var ArtDirectionResponseSchema = z35.object({
35261
- fields: z35.record(z35.string(), z35.string())
35431
+ var ArtDirectionResponseSchema = z37.object({
35432
+ fields: z37.record(z37.string(), z37.string())
35262
35433
  });
35263
35434
  var KNOWN_KEYS = /* @__PURE__ */ new Set([
35264
35435
  "art-direction.intent",
@@ -35413,45 +35584,6 @@ async function generateAndApplyArtDirection(options) {
35413
35584
  return { generatedCount: acceptedUpdates.size };
35414
35585
  }
35415
35586
 
35416
- // ../server/src/server/claude-profile.ts
35417
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync, rmSync as rmSync2 } from "node:fs";
35418
- import path20 from "node:path";
35419
- import os8 from "node:os";
35420
- var SHARED_ITEMS = ["settings.json", "hooks", "agents", "skills", "plugins", "keybindings.json"];
35421
- function getClaudeProfileDir(username) {
35422
- return path20.join(os8.homedir(), `.claude-${username}`);
35423
- }
35424
- function ensureClaudeProfile(username, logger) {
35425
- const profileDir = getClaudeProfileDir(username);
35426
- const ownerDir = path20.join(os8.homedir(), ".claude");
35427
- if (!existsSync10(ownerDir)) {
35428
- throw new Error(`Owner claude config dir not found: ${ownerDir}`);
35429
- }
35430
- if (!existsSync10(profileDir)) {
35431
- mkdirSync5(profileDir, { recursive: true });
35432
- logger?.info({ profileDir, username }, "created claude profile directory");
35433
- }
35434
- for (const item of SHARED_ITEMS) {
35435
- const target = path20.join(ownerDir, item);
35436
- const link = path20.join(profileDir, item);
35437
- if (!existsSync10(target)) continue;
35438
- if (existsSync10(link)) continue;
35439
- symlinkSync(target, link);
35440
- logger?.info({ item, profileDir }, "symlinked shared config item");
35441
- }
35442
- return profileDir;
35443
- }
35444
- function hasClaudeAuth(username) {
35445
- const profileDir = getClaudeProfileDir(username);
35446
- return existsSync10(profileDir);
35447
- }
35448
- function removeClaudeProfile(username, logger) {
35449
- const profileDir = getClaudeProfileDir(username);
35450
- if (!existsSync10(profileDir)) return;
35451
- rmSync2(profileDir, { recursive: true, force: true });
35452
- logger?.info({ profileDir, username }, "removed claude profile directory");
35453
- }
35454
-
35455
35587
  // ../server/src/services/oauth-service.ts
35456
35588
  import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
35457
35589
  import { mkdir as mkdir4, readFile as readFile3, rename, unlink, writeFile as writeFile4 } from "node:fs/promises";
@@ -36843,7 +36975,7 @@ var MIN_STREAMING_SEGMENT_DURATION_MS = 1e3;
36843
36975
  var MIN_STREAMING_SEGMENT_BYTES = Math.round(
36844
36976
  PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS
36845
36977
  );
36846
- var AgentIdSchema = z36.string().uuid();
36978
+ var AgentIdSchema2 = z38.string().uuid();
36847
36979
  var VOICE_INTERRUPT_CONFIRMATION_MS = 500;
36848
36980
  var VoiceFeatureUnavailableError = class extends Error {
36849
36981
  constructor(context) {
@@ -36949,6 +37081,8 @@ var Session = class _Session {
36949
37081
  const {
36950
37082
  clientId,
36951
37083
  appVersion,
37084
+ peerPublicKeyB64,
37085
+ ownerUserId,
36952
37086
  onMessage,
36953
37087
  onBinaryMessage,
36954
37088
  onLifecycleIntent,
@@ -37011,6 +37145,8 @@ var Session = class _Session {
37011
37145
  });
37012
37146
  this.agentManager = agentManager;
37013
37147
  this.agentStorage = agentStorage;
37148
+ this.peerPublicKeyB64 = peerPublicKeyB64 ?? null;
37149
+ this.ownerUserId = ownerUserId ?? null;
37014
37150
  this.uploadStore = new SessionUploadStore({ logger: this.sessionLogger });
37015
37151
  this.imageStore = new SessionImageStore({ logger: this.sessionLogger });
37016
37152
  this.projectRegistry = projectRegistry;
@@ -37085,7 +37221,43 @@ var Session = class _Session {
37085
37221
  });
37086
37222
  void this.initializeAgentMcp();
37087
37223
  this.subscribeToAgentEvents();
37088
- this.sessionLogger.trace("Session created");
37224
+ this.sessionLogger.trace(
37225
+ {
37226
+ hasPeerPubkey: this.peerPublicKeyB64 !== null,
37227
+ // Log a fingerprint, not the full key, so the audit log stays useful
37228
+ // without bloating every entry with 44-char base64. First 8 chars is
37229
+ // unique enough for human cross-reference.
37230
+ peerPubkeyPrefix: this.peerPublicKeyB64 ? this.peerPublicKeyB64.slice(0, 8) : null,
37231
+ ownerUserId: this.ownerUserId
37232
+ },
37233
+ "Session created"
37234
+ );
37235
+ }
37236
+ /**
37237
+ * Peer device's e2ee pubkey (base64). Surfaced so the upcoming user-scope
37238
+ * lookup can resolve it via the auth-server allow-list. Null when unknown
37239
+ * (local TCP, legacy paths).
37240
+ */
37241
+ getPeerPublicKeyB64() {
37242
+ return this.peerPublicKeyB64;
37243
+ }
37244
+ /**
37245
+ * Auth-server user-id that owns the connecting device. Null when the
37246
+ * daemon has no auth-server linkage, when the peer pubkey is unknown
37247
+ * (local TCP), or when the resolver returned no match (allow-list entry
37248
+ * for this pubkey hasn't been seen yet). Callers use this for per-user
37249
+ * agent filtering; null means "show everything" (single-tenant fallback).
37250
+ */
37251
+ getOwnerUserId() {
37252
+ return this.ownerUserId;
37253
+ }
37254
+ /**
37255
+ * Whether this session is bound to a specific account. Equivalent to
37256
+ * `getOwnerUserId() !== null` but reads cleaner at call sites that gate
37257
+ * on "should we apply per-user filtering at all?".
37258
+ */
37259
+ hasOwnerUserId() {
37260
+ return this.ownerUserId !== null;
37089
37261
  }
37090
37262
  updateAppVersion(appVersion) {
37091
37263
  if (appVersion && appVersion !== this.appVersion) {
@@ -37330,6 +37502,9 @@ var Session = class _Session {
37330
37502
  );
37331
37503
  });
37332
37504
  }
37505
+ if (this.ownerUserId !== null && !this.agentManager.canUserAccessAgentById(event.agentId, this.ownerUserId)) {
37506
+ return;
37507
+ }
37333
37508
  const activity = this.clientActivity;
37334
37509
  if (activity?.deviceType === "mobile") {
37335
37510
  if (!activity.focusedAgentId) {
@@ -37557,6 +37732,9 @@ var Session = class _Session {
37557
37732
  }
37558
37733
  async forwardAgentUpdate(agent) {
37559
37734
  try {
37735
+ if (this.ownerUserId !== null && !this.agentManager.canUserAccessAgentById(agent.id, this.ownerUserId)) {
37736
+ return;
37737
+ }
37560
37738
  const subscription = this.agentUpdatesSubscription;
37561
37739
  const payload = await this.buildAgentPayload(agent);
37562
37740
  if (subscription) {
@@ -37671,6 +37849,15 @@ var Session = class _Session {
37671
37849
  case "delete_session_upload_request":
37672
37850
  await this.handleDeleteSessionUploadRequest(msg);
37673
37851
  break;
37852
+ case "share_agent_with_user_request":
37853
+ await this.handleShareAgentWithUserRequest(msg);
37854
+ break;
37855
+ case "unshare_agent_with_user_request":
37856
+ await this.handleUnshareAgentWithUserRequest(msg);
37857
+ break;
37858
+ case "list_agent_shared_users_request":
37859
+ await this.handleListAgentSharedUsersRequest(msg);
37860
+ break;
37674
37861
  case "list_session_images_request":
37675
37862
  await this.handleListSessionImagesRequest(msg);
37676
37863
  break;
@@ -38969,7 +39156,7 @@ var Session = class _Session {
38969
39156
  }
38970
39157
  }
38971
39158
  parseVoiceTargetAgentId(rawId, source) {
38972
- const parsed = AgentIdSchema.safeParse(rawId.trim());
39159
+ const parsed = AgentIdSchema2.safeParse(rawId.trim());
38973
39160
  if (!parsed.success) {
38974
39161
  throw new Error(`${source}: agentId must be a UUID`);
38975
39162
  }
@@ -39298,6 +39485,12 @@ var Session = class _Session {
39298
39485
  labels,
39299
39486
  workspaceId: resolvedWorkspace.workspaceId,
39300
39487
  initialPrompt: trimmedPrompt,
39488
+ // Stamp the agent with the user that created it. Null on single-
39489
+ // tenant daemons (no auth-server linkage) — agent stays globally
39490
+ // visible, preserving legacy behavior. This is the authoritative
39491
+ // owner (pubkey-derived), used for `canAccessAgent`.
39492
+ ownerUserId: this.ownerUserId,
39493
+ // Self-declared identity, used for Claude profile dir naming etc.
39301
39494
  userId: this.userId ?? void 0,
39302
39495
  username: this.username ?? void 0
39303
39496
  }
@@ -39778,8 +39971,8 @@ var Session = class _Session {
39778
39971
  }
39779
39972
  async generateCommitMessage(cwd) {
39780
39973
  const files = await listUncommittedFiles(cwd);
39781
- const schema = z36.object({
39782
- message: z36.string().min(1).max(100).describe(
39974
+ const schema = z38.object({
39975
+ message: z38.string().min(1).max(100).describe(
39783
39976
  "Short feature-level summary, lowercase, imperative mood, no trailing period, no filename references."
39784
39977
  )
39785
39978
  });
@@ -39824,9 +40017,9 @@ var Session = class _Session {
39824
40017
  },
39825
40018
  { appostleHome: this.appostleHome }
39826
40019
  );
39827
- const schema = z36.object({
39828
- title: z36.string().min(1).max(72),
39829
- body: z36.string().min(1)
40020
+ const schema = z38.object({
40021
+ title: z38.string().min(1).max(72),
40022
+ body: z38.string().min(1)
39830
40023
  });
39831
40024
  const fileList = diff.structured && diff.structured.length > 0 ? [
39832
40025
  "Files changed:",
@@ -41866,13 +42059,22 @@ ${details}`.trim());
41866
42059
  * Build the current agent list payload (live + persisted), optionally filtered by labels.
41867
42060
  */
41868
42061
  async listAgentPayloads(filter) {
41869
- const agentSnapshots = this.agentManager.listAgents();
42062
+ const agentSnapshots = this.agentManager.listAgentsForUser(this.ownerUserId);
41870
42063
  const liveAgents = await Promise.all(
41871
42064
  agentSnapshots.map((agent) => this.buildAgentPayload(agent))
41872
42065
  );
41873
42066
  const registryRecords = await this.agentStorage.list();
42067
+ const requesterId = this.ownerUserId;
41874
42068
  const liveIds = new Set(agentSnapshots.map((a) => a.id));
41875
- const persistedAgents = registryRecords.filter((record) => !liveIds.has(record.id)).map((record) => this.buildStoredAgentPayload(record));
42069
+ const persistedAgents = registryRecords.filter((record) => !liveIds.has(record.id)).filter(
42070
+ (record) => requesterId === null || canUserAccessAgent(
42071
+ {
42072
+ ownerUserId: record.ownerUserId ?? null,
42073
+ sharedWithUserIds: record.sharedWithUserIds
42074
+ },
42075
+ requesterId
42076
+ )
42077
+ ).map((record) => this.buildStoredAgentPayload(record));
41876
42078
  let agents = [...liveAgents, ...persistedAgents];
41877
42079
  agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
41878
42080
  if (filter?.labels) {
@@ -41889,6 +42091,9 @@ ${details}`.trim());
41889
42091
  return { ok: false, error: "Agent identifier cannot be empty" };
41890
42092
  }
41891
42093
  if (this.agentManager.getAgent(trimmed)) {
42094
+ if (!this.agentManager.canUserAccessAgentById(trimmed, this.ownerUserId)) {
42095
+ return { ok: false, error: "Agent not found" };
42096
+ }
41892
42097
  return { ok: true, agentId: trimmed };
41893
42098
  }
41894
42099
  const exactStored = await this.agentStorage.get(trimmed);
@@ -43492,6 +43697,139 @@ ${details}`.trim());
43492
43697
  }
43493
43698
  }
43494
43699
  // ──────────────────────────────────────────────────────────────────────
43700
+ // Phase 4: agent sharing — owner-only mutations of the access ACL.
43701
+ // The visibility predicate (Phase 2c) already honors `sharedWithUserIds`;
43702
+ // these handlers surface a typed user-facing path so an owner can grant /
43703
+ // revoke / inspect access from the app instead of editing JSON on disk.
43704
+ //
43705
+ // Authorization:
43706
+ // - All three handlers require the session's `ownerUserId` to equal the
43707
+ // agent's `ownerUserId`. Anyone else gets `unauthorized` (including
43708
+ // existing share-recipients — only the owner can re-share).
43709
+ // - `resolveAgentIdentifier` already applies `canUserAccessAgent`, so a
43710
+ // non-recipient even sees the agent as "not found". The owner check
43711
+ // here is the strictly-tighter follow-up gate for mutations.
43712
+ // - On a single-tenant daemon (session.ownerUserId == null) the gate
43713
+ // opens — no isolation to enforce, sharing is a no-op for unscoped
43714
+ // agents (their ownerUserId is also null).
43715
+ // ──────────────────────────────────────────────────────────────────────
43716
+ async handleShareAgentWithUserRequest(msg) {
43717
+ const resolved = await this.resolveAgentIdentifier(msg.agentId);
43718
+ if (!resolved.ok) {
43719
+ this.emit({
43720
+ type: "share_agent_with_user_response",
43721
+ payload: { requestId: msg.requestId, ok: false, error: resolved.error }
43722
+ });
43723
+ return;
43724
+ }
43725
+ const agent = this.agentManager.getAgent(resolved.agentId);
43726
+ if (!agent) {
43727
+ this.emit({
43728
+ type: "share_agent_with_user_response",
43729
+ payload: { requestId: msg.requestId, ok: false, error: "Agent not found" }
43730
+ });
43731
+ return;
43732
+ }
43733
+ if (this.ownerUserId !== null && agent.ownerUserId !== null && agent.ownerUserId !== this.ownerUserId) {
43734
+ this.emit({
43735
+ type: "share_agent_with_user_response",
43736
+ payload: { requestId: msg.requestId, ok: false, error: "unauthorized" }
43737
+ });
43738
+ return;
43739
+ }
43740
+ try {
43741
+ const sharedWithUserIds = await this.agentManager.shareAgentWithUser(
43742
+ resolved.agentId,
43743
+ msg.userId
43744
+ );
43745
+ this.emit({
43746
+ type: "share_agent_with_user_response",
43747
+ payload: { requestId: msg.requestId, ok: true, sharedWithUserIds }
43748
+ });
43749
+ } catch (error) {
43750
+ const message = error instanceof Error ? error.message : String(error);
43751
+ this.emit({
43752
+ type: "share_agent_with_user_response",
43753
+ payload: { requestId: msg.requestId, ok: false, error: message }
43754
+ });
43755
+ }
43756
+ }
43757
+ async handleUnshareAgentWithUserRequest(msg) {
43758
+ const resolved = await this.resolveAgentIdentifier(msg.agentId);
43759
+ if (!resolved.ok) {
43760
+ this.emit({
43761
+ type: "unshare_agent_with_user_response",
43762
+ payload: { requestId: msg.requestId, ok: false, error: resolved.error }
43763
+ });
43764
+ return;
43765
+ }
43766
+ const agent = this.agentManager.getAgent(resolved.agentId);
43767
+ if (!agent) {
43768
+ this.emit({
43769
+ type: "unshare_agent_with_user_response",
43770
+ payload: { requestId: msg.requestId, ok: false, error: "Agent not found" }
43771
+ });
43772
+ return;
43773
+ }
43774
+ if (this.ownerUserId !== null && agent.ownerUserId !== null && agent.ownerUserId !== this.ownerUserId) {
43775
+ this.emit({
43776
+ type: "unshare_agent_with_user_response",
43777
+ payload: { requestId: msg.requestId, ok: false, error: "unauthorized" }
43778
+ });
43779
+ return;
43780
+ }
43781
+ try {
43782
+ const sharedWithUserIds = await this.agentManager.unshareAgentWithUser(
43783
+ resolved.agentId,
43784
+ msg.userId
43785
+ );
43786
+ this.emit({
43787
+ type: "unshare_agent_with_user_response",
43788
+ payload: { requestId: msg.requestId, ok: true, sharedWithUserIds }
43789
+ });
43790
+ } catch (error) {
43791
+ const message = error instanceof Error ? error.message : String(error);
43792
+ this.emit({
43793
+ type: "unshare_agent_with_user_response",
43794
+ payload: { requestId: msg.requestId, ok: false, error: message }
43795
+ });
43796
+ }
43797
+ }
43798
+ async handleListAgentSharedUsersRequest(msg) {
43799
+ const resolved = await this.resolveAgentIdentifier(msg.agentId);
43800
+ if (!resolved.ok) {
43801
+ this.emit({
43802
+ type: "list_agent_shared_users_response",
43803
+ payload: { requestId: msg.requestId, ok: false, error: resolved.error }
43804
+ });
43805
+ return;
43806
+ }
43807
+ const agent = this.agentManager.getAgent(resolved.agentId);
43808
+ if (!agent) {
43809
+ this.emit({
43810
+ type: "list_agent_shared_users_response",
43811
+ payload: { requestId: msg.requestId, ok: false, error: "Agent not found" }
43812
+ });
43813
+ return;
43814
+ }
43815
+ if (this.ownerUserId !== null && agent.ownerUserId !== null && agent.ownerUserId !== this.ownerUserId) {
43816
+ this.emit({
43817
+ type: "list_agent_shared_users_response",
43818
+ payload: { requestId: msg.requestId, ok: false, error: "unauthorized" }
43819
+ });
43820
+ return;
43821
+ }
43822
+ this.emit({
43823
+ type: "list_agent_shared_users_response",
43824
+ payload: {
43825
+ requestId: msg.requestId,
43826
+ ok: true,
43827
+ ownerUserId: agent.ownerUserId,
43828
+ sharedWithUserIds: [...agent.sharedWithUserIds]
43829
+ }
43830
+ });
43831
+ }
43832
+ // ──────────────────────────────────────────────────────────────────────
43495
43833
  // Session images — backs the "Images" tab inside the uploads modal. Same
43496
43834
  // shape as the file handlers above; the store has no manifest, so listing
43497
43835
  // is a directory scan and delete is `unlink` (traversal-guarded).
@@ -45666,7 +46004,7 @@ import webpush from "web-push";
45666
46004
  import webpush2 from "web-push";
45667
46005
 
45668
46006
  // ../server/src/server/speech/providers/local/sherpa/model-catalog.ts
45669
- import { z as z37 } from "zod";
46007
+ import { z as z39 } from "zod";
45670
46008
  var SHERPA_ONNX_MODEL_CATALOG = {
45671
46009
  "zipformer-bilingual-zh-en-2023-02-20": {
45672
46010
  kind: "stt-online",
@@ -45759,7 +46097,7 @@ function buildAliasMap(modelIds) {
45759
46097
  }
45760
46098
  function createAliasedModelIdSchema(params) {
45761
46099
  const validIds = new Set(params.modelIds);
45762
- return z37.string().trim().toLowerCase().refine(
46100
+ return z39.string().trim().toLowerCase().refine(
45763
46101
  (value) => validIds.has(value) || Object.prototype.hasOwnProperty.call(params.aliases, value),
45764
46102
  {
45765
46103
  message: "Invalid model id"
@@ -45790,20 +46128,20 @@ import { v4 as uuidv410 } from "uuid";
45790
46128
  import { v4 as uuidv411 } from "uuid";
45791
46129
 
45792
46130
  // ../server/src/server/speech/providers/openai/config.ts
45793
- import { z as z38 } from "zod";
46131
+ import { z as z40 } from "zod";
45794
46132
  var DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
45795
46133
  var DEFAULT_OPENAI_TTS_MODEL = "tts-1";
45796
- var OpenAiTtsVoiceSchema = z38.enum(["alloy", "echo", "fable", "onyx", "nova", "shimmer"]);
45797
- var OpenAiTtsModelSchema = z38.enum(["tts-1", "tts-1-hd"]);
45798
- var NumberLikeSchema = z38.union([z38.number(), z38.string().trim().min(1)]);
45799
- var OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z38.coerce.number().finite()).optional();
45800
- var OptionalTrimmedStringSchema = z38.string().trim().optional().transform((value) => value && value.length > 0 ? value : void 0);
45801
- var OpenAiSpeechResolutionSchema = z38.object({
46134
+ var OpenAiTtsVoiceSchema = z40.enum(["alloy", "echo", "fable", "onyx", "nova", "shimmer"]);
46135
+ var OpenAiTtsModelSchema = z40.enum(["tts-1", "tts-1-hd"]);
46136
+ var NumberLikeSchema = z40.union([z40.number(), z40.string().trim().min(1)]);
46137
+ var OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z40.coerce.number().finite()).optional();
46138
+ var OptionalTrimmedStringSchema = z40.string().trim().optional().transform((value) => value && value.length > 0 ? value : void 0);
46139
+ var OpenAiSpeechResolutionSchema = z40.object({
45802
46140
  apiKey: OptionalTrimmedStringSchema,
45803
46141
  sttConfidenceThreshold: OptionalFiniteNumberSchema,
45804
46142
  sttModel: OptionalTrimmedStringSchema,
45805
- ttsVoice: z38.string().trim().toLowerCase().pipe(OpenAiTtsVoiceSchema).default("alloy"),
45806
- ttsModel: z38.string().trim().toLowerCase().pipe(OpenAiTtsModelSchema).default(DEFAULT_OPENAI_TTS_MODEL),
46143
+ ttsVoice: z40.string().trim().toLowerCase().pipe(OpenAiTtsVoiceSchema).default("alloy"),
46144
+ ttsModel: z40.string().trim().toLowerCase().pipe(OpenAiTtsModelSchema).default(DEFAULT_OPENAI_TTS_MODEL),
45807
46145
  realtimeTranscriptionModel: OptionalTrimmedStringSchema.default(
45808
46146
  DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL
45809
46147
  )
@@ -45847,17 +46185,6 @@ import { v4 } from "uuid";
45847
46185
  // ../server/src/server/speech/providers/openai/tts.ts
45848
46186
  import OpenAI2 from "openai";
45849
46187
 
45850
- // ../server/src/server/agent/agent-manager.ts
45851
- import { z as z40 } from "zod";
45852
- import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
45853
-
45854
- // ../server/src/server/agent/handoff-mcp.ts
45855
- import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
45856
- import { z as z39 } from "zod";
45857
-
45858
- // ../server/src/server/agent/agent-manager.ts
45859
- var AgentIdSchema2 = z40.string().uuid();
45860
-
45861
46188
  // ../server/src/server/agent/agent-storage.ts
45862
46189
  import { z as z41 } from "zod";
45863
46190
  var SERIALIZABLE_CONFIG_SCHEMA = z41.object({
@@ -45907,7 +46234,22 @@ var STORED_AGENT_SCHEMA = z41.object({
45907
46234
  archivedAt: z41.string().nullable().optional(),
45908
46235
  // Fork lineage (optional for backward compat with pre-fork records).
45909
46236
  parentAgentId: z41.string().optional(),
45910
- forkedFromMessageUuid: z41.string().optional()
46237
+ forkedFromMessageUuid: z41.string().optional(),
46238
+ // Multi-tenant session isolation: the auth-server user-id that created
46239
+ // (and therefore owns) this agent + the additive ACL of other users
46240
+ // granted access. Both optional/null-default for backward compatibility
46241
+ // with pre-Phase-2c records — those load with `null` owner and stay
46242
+ // visible to every connecting user (matches today's single-tenant
46243
+ // behavior). Set on new agents at create time via Session.ownerUserId.
46244
+ ownerUserId: z41.string().nullable().optional(),
46245
+ sharedWithUserIds: z41.array(z41.string()).default([]),
46246
+ // Owner's display username — needed on resume to route to the correct
46247
+ // `CLAUDE_CONFIG_DIR` via `ensureClaudeProfile(username)` for agents
46248
+ // created by a shared (non-owner) user. Without this, a resumed shared-
46249
+ // user agent would silently fall back to the daemon owner's Claude
46250
+ // profile/subscription. Optional — pre-Phase-3 records rehydrate without
46251
+ // per-user profile routing.
46252
+ ownerUsername: z41.string().optional()
45911
46253
  });
45912
46254
 
45913
46255
  // ../server/src/server/agent/mcp-server.ts
@@ -46591,10 +46933,10 @@ function encodeUtf8String(value) {
46591
46933
  function createRelayE2eeTransportFactory(args) {
46592
46934
  return ({ url, headers }) => {
46593
46935
  const base = args.baseFactory({ url, headers });
46594
- return createEncryptedTransport(base, args.daemonPublicKeyB64, args.logger);
46936
+ return createEncryptedTransport(base, args.daemonPublicKeyB64, args.logger, args.staticKeyPair);
46595
46937
  };
46596
46938
  }
46597
- function createEncryptedTransport(base, daemonPublicKeyB64, logger) {
46939
+ function createEncryptedTransport(base, daemonPublicKeyB64, logger, staticKeyPair) {
46598
46940
  let channel = null;
46599
46941
  let opened = false;
46600
46942
  let closed = false;
@@ -46651,12 +46993,17 @@ function createEncryptedTransport(base, daemonPublicKeyB64, logger) {
46651
46993
  };
46652
46994
  const startHandshake = async () => {
46653
46995
  try {
46654
- channel = await createClientChannel(relayTransport, daemonPublicKeyB64, {
46655
- onopen: emitOpen,
46656
- onmessage: (data) => emitMessage(data),
46657
- onclose: (code, reason) => emitClose({ code, reason }),
46658
- onerror: (error) => emitError(error)
46659
- });
46996
+ channel = await createClientChannel(
46997
+ relayTransport,
46998
+ daemonPublicKeyB64,
46999
+ {
47000
+ onopen: emitOpen,
47001
+ onmessage: (data) => emitMessage(data),
47002
+ onclose: (code, reason) => emitClose({ code, reason }),
47003
+ onerror: (error) => emitError(error)
47004
+ },
47005
+ staticKeyPair
47006
+ );
46660
47007
  } catch (error) {
46661
47008
  logger.warn({ err: normalizeTransportError(error) }, "relay_e2ee_handshake_failed");
46662
47009
  emitError(error);
@@ -47019,7 +47366,8 @@ var DaemonClient = class {
47019
47366
  transportFactory = createRelayE2eeTransportFactory({
47020
47367
  baseFactory: baseTransportFactory,
47021
47368
  daemonPublicKeyB64,
47022
- logger: this.logger
47369
+ logger: this.logger,
47370
+ staticKeyPair: this.config.e2ee?.staticKeyPair
47023
47371
  });
47024
47372
  }
47025
47373
  const transportUrl = this.resolveTransportUrlForAttempt();
@@ -48093,6 +48441,99 @@ var DaemonClient = class {
48093
48441
  throw new Error(payload.error || "Failed to delete session upload");
48094
48442
  }
48095
48443
  }
48444
+ // ──────────────────────────────────────────────────────────────────────
48445
+ // Phase 4 agent sharing — owner-only mutations of the per-agent ACL.
48446
+ // ──────────────────────────────────────────────────────────────────────
48447
+ /**
48448
+ * Grant a user access to this agent. Owner-only. Idempotent — sharing
48449
+ * with a user already on the ACL is a no-op. The recipient does not
48450
+ * need to be currently paired; access takes effect at their next
48451
+ * connection.
48452
+ */
48453
+ async shareAgentWithUser(agentId, userId) {
48454
+ const requestId = this.createRequestId();
48455
+ const message = SessionInboundMessageSchema.parse({
48456
+ type: "share_agent_with_user_request",
48457
+ requestId,
48458
+ agentId,
48459
+ userId
48460
+ });
48461
+ const payload = await this.sendRequest({
48462
+ requestId,
48463
+ message,
48464
+ timeout: 15e3,
48465
+ options: { skipQueue: true },
48466
+ select: (msg) => {
48467
+ if (msg.type !== "share_agent_with_user_response") return null;
48468
+ if (msg.payload.requestId !== requestId) return null;
48469
+ return msg.payload;
48470
+ }
48471
+ });
48472
+ if (!payload.ok) {
48473
+ throw new Error(payload.error || "Failed to share agent");
48474
+ }
48475
+ return payload.sharedWithUserIds;
48476
+ }
48477
+ /**
48478
+ * Revoke a user's access to this agent. Owner-only. Effects are
48479
+ * immediate — once the auth-server allow-list propagates (and the
48480
+ * daemon's in-memory state updates here), the removed user's resolver
48481
+ * answers "Agent not found" for any further per-agent RPC.
48482
+ */
48483
+ async unshareAgentWithUser(agentId, userId) {
48484
+ const requestId = this.createRequestId();
48485
+ const message = SessionInboundMessageSchema.parse({
48486
+ type: "unshare_agent_with_user_request",
48487
+ requestId,
48488
+ agentId,
48489
+ userId
48490
+ });
48491
+ const payload = await this.sendRequest({
48492
+ requestId,
48493
+ message,
48494
+ timeout: 15e3,
48495
+ options: { skipQueue: true },
48496
+ select: (msg) => {
48497
+ if (msg.type !== "unshare_agent_with_user_response") return null;
48498
+ if (msg.payload.requestId !== requestId) return null;
48499
+ return msg.payload;
48500
+ }
48501
+ });
48502
+ if (!payload.ok) {
48503
+ throw new Error(payload.error || "Failed to unshare agent");
48504
+ }
48505
+ return payload.sharedWithUserIds;
48506
+ }
48507
+ /**
48508
+ * Return the ACL — owner + the user-ids the owner has granted access
48509
+ * to. Owner-only: recipients can't enumerate who else has access.
48510
+ */
48511
+ async listAgentSharedUsers(agentId) {
48512
+ const requestId = this.createRequestId();
48513
+ const message = SessionInboundMessageSchema.parse({
48514
+ type: "list_agent_shared_users_request",
48515
+ requestId,
48516
+ agentId
48517
+ });
48518
+ const payload = await this.sendRequest({
48519
+ requestId,
48520
+ message,
48521
+ timeout: 15e3,
48522
+ options: { skipQueue: true },
48523
+ select: (msg) => {
48524
+ if (msg.type !== "list_agent_shared_users_response") return null;
48525
+ if (msg.payload.requestId !== requestId) return null;
48526
+ return msg.payload;
48527
+ }
48528
+ });
48529
+ if (!payload.ok) {
48530
+ throw new Error(payload.error || "Failed to list agent shared users");
48531
+ }
48532
+ return {
48533
+ ownerUserId: payload.ownerUserId,
48534
+ sharedWithUserIds: payload.sharedWithUserIds
48535
+ };
48536
+ }
48096
48537
  /**
48097
48538
  * List the images the user has attached to this agent's chat (the "Images"
48098
48539
  * tab in the uploads modal). Returns newest first; the daemon scans