@wirecat/cli-core 0.448.0 → 0.450.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 (43) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/dist/cli/command-availability.d.ts +2 -1
  3. package/dist/cli/command-availability.js +8 -5
  4. package/dist/cli/config-command.d.ts +3 -3
  5. package/dist/cli/config-command.js +2 -2
  6. package/dist/cli/early-refusal.d.ts +13 -0
  7. package/dist/cli/early-refusal.js +44 -2
  8. package/dist/cli/index.d.ts +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/cli/mcp-server-command.js +9 -5
  11. package/dist/cli/messenger/mcp-command.d.ts +4 -1
  12. package/dist/cli/messenger/mcp-command.js +108 -12
  13. package/dist/cli/permission-keys.d.ts +21 -4
  14. package/dist/cli/permission-keys.js +31 -1
  15. package/dist/cli/preview.d.ts +2 -1
  16. package/dist/cli/preview.js +8 -6
  17. package/dist/cli/program.d.ts +3 -0
  18. package/dist/cli/program.js +10 -3
  19. package/dist/cli/settings.d.ts +9 -2
  20. package/dist/cli/settings.js +21 -6
  21. package/dist/cli/shared-command-exports.d.ts +7 -5
  22. package/dist/http/public-url.d.ts +5 -1
  23. package/dist/http/public-url.js +11 -3
  24. package/dist/mcp/agent.d.ts +5 -0
  25. package/dist/mcp/agent.js +7 -4
  26. package/dist/mcp/all-profiles.d.ts +47 -0
  27. package/dist/mcp/all-profiles.js +257 -0
  28. package/dist/mcp/http/oauth.d.ts +2 -2
  29. package/dist/mcp/http/oauth.js +8 -7
  30. package/dist/mcp/http/serve.d.ts +6 -1
  31. package/dist/mcp/http/serve.js +24 -7
  32. package/dist/mcp/index.d.ts +2 -1
  33. package/dist/mcp/index.js +2 -1
  34. package/dist/mcp/instructions.d.ts +2 -1
  35. package/dist/mcp/instructions.js +35 -29
  36. package/dist/mcp/server.js +3 -73
  37. package/dist/mcp/stdio-wire.d.ts +14 -0
  38. package/dist/mcp/stdio-wire.js +72 -0
  39. package/dist/sends/index.d.ts +1 -1
  40. package/dist/sends/index.js +1 -1
  41. package/dist/sends/permissions.d.ts +10 -2
  42. package/dist/sends/permissions.js +15 -7
  43. package/package.json +3 -3
@@ -6,19 +6,21 @@ import { commandPathOf } from "./command-contract.js";
6
6
  import { refuseUnknownPermissionKeys } from "./permission-keys.js";
7
7
  export class PreviewComplete extends Error {
8
8
  }
9
- export const preview = (command, settings, tool, keyOf, messenger = true) => {
9
+ export const preview = (command, settings, tool, keys, messenger = true) => {
10
10
  const path = commandPathOf(command);
11
11
  const { permissions } = settings;
12
- refuseUnknownPermissionKeys(command, settings, tool, keyOf && ((one) => keyOf(commandPathOf(one))));
13
- const key = keyOf?.(path) ?? keyForCommand(path);
14
- const level = key ? levelFor(permissions, key).level : "allow";
15
- for (const source of readKeysForCommand(path)) {
12
+ const declared = typeof keys === "object" ? keys : undefined;
13
+ const keyOf = typeof keys === "object" ? keys.keyOf : keys;
14
+ refuseUnknownPermissionKeys(command, settings, tool, declared ?? (keyOf && ((one) => keyOf(commandPathOf(one)))));
15
+ const key = declared ? declared.keyOf(path) : (keyOf?.(path) ?? keyForCommand(path));
16
+ const level = key ? levelFor(permissions, key, declared?.defaults).level : "allow";
17
+ for (const source of declared ? [] : readKeysForCommand(path)) {
16
18
  if (levelFor(permissions, source).level === "deny")
17
19
  throw new CliError("permission_error", `this profile does not permit ${source}`, { permission: source });
18
20
  }
19
21
  const meta = metaOf(command);
20
22
  const writes = (meta.effects !== undefined && writesAnything(meta.effects)) ||
21
- (key !== null && key !== undefined && WRITE_KEYS.includes(key));
23
+ (!declared && key !== null && key !== undefined && WRITE_KEYS.includes(key));
22
24
  if (level === "deny" || (writes && level === "readonly"))
23
25
  throw new CliError("permission_error", `this profile does not permit ${path.join(" ")}`, { permission: key });
24
26
  return previewBody(command, { key: key ?? null, level }, messenger);
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { type AppIdentity } from "../ports/app.js";
3
3
  import { type BaseEnvironment } from "./context.js";
4
4
  import { type FirstScreen } from "./help/first-screen.js";
5
+ import type { CommandPermissions } from "./permission-keys.js";
5
6
  import { type GlobalFlags, type ResolveOptions, type Settings } from "./settings.js";
6
7
  export interface ProgramDefinition {
7
8
  app: AppIdentity;
@@ -10,6 +11,8 @@ export interface ProgramDefinition {
10
11
  /** The CLI's own settings, for keeping a failure that happened before its command could; plain ones without. */
11
12
  configuration?: {
12
13
  permissionKey?: (path: readonly string[]) => string | null | undefined;
14
+ /** A CLI's own keys (`commandPermissions`), from `settingsFor`: its commands are refused by them before they run. */
15
+ permissions?: CommandPermissions | undefined;
13
16
  resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "recordInput" | "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
14
17
  };
15
18
  configure?: (program: Command) => void;
@@ -17,7 +17,7 @@ import { logParsedArgv, logVersion } from "./argv-log.js";
17
17
  import { commandAvailability } from "./command-availability.js";
18
18
  import { commandPathOf } from "./command-contract.js";
19
19
  import { outputFor, provide } from "./context.js";
20
- import { refuseWriteBeforeConnecting } from "./early-refusal.js";
20
+ import { refuseByCommandPermissions, refuseWriteBeforeConnecting } from "./early-refusal.js";
21
21
  import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
22
22
  import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
23
23
  import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
@@ -221,7 +221,7 @@ const runOnce = async (argv, definition, options) => {
221
221
  });
222
222
  if (!settings.permissions)
223
223
  return undefined;
224
- return commandAvailability(info, settings.permissions, definition.configuration?.permissionKey, changesAuthorityAt(program), readOnlyLockOf(definition.app, env) !== undefined, options.agent !== undefined);
224
+ return commandAvailability(info, settings.permissions, definition.configuration?.permissionKey ?? definition.configuration?.permissions?.keyOf, changesAuthorityAt(program), readOnlyLockOf(definition.app, env) !== undefined, options.agent !== undefined, definition.configuration?.permissions);
225
225
  }
226
226
  catch {
227
227
  // Help must still work when the configuration needs repair.
@@ -296,6 +296,13 @@ const runOnce = async (argv, definition, options) => {
296
296
  dryRun: flags.dryRun === true,
297
297
  ...(definition.configuration ? { resolve: definition.configuration.resolveSettings } : {}),
298
298
  });
299
+ if (definition.configuration?.permissions)
300
+ refuseByCommandPermissions(action, definition.configuration.permissions, {
301
+ app: definition.app,
302
+ env: commandEnv,
303
+ dryRun: flags.dryRun === true,
304
+ resolve: definition.configuration.resolveSettings,
305
+ });
299
306
  // A client older than the relay's refusal to forward a login still sends one; on serve it would log out serve.
300
307
  if (options.relayed && isLogin(action))
301
308
  throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` logs the account in or out, which serve never runs for a client — stop ` +
@@ -394,7 +401,7 @@ const runOnce = async (argv, definition, options) => {
394
401
  configPath: resolved.configPath ?? "",
395
402
  profile: resolved.profile,
396
403
  ...(resolved.savedPermissions ? { savedPermissions: resolved.savedPermissions } : {}),
397
- }, command, definition.configuration?.permissionKey, definition.app.messenger !== false);
404
+ }, command, definition.configuration?.permissionKey ?? definition.configuration?.permissions, definition.app.messenger !== false);
398
405
  renderAnswer(action, outputFor(action), itemOf(snakeKeys(previewed)));
399
406
  throw new PreviewComplete();
400
407
  }
@@ -4,6 +4,7 @@ import { type AISettings } from "../analysis/settings.js";
4
4
  import { type AppIdentity } from "../ports/app.js";
5
5
  import { type Level, type PermissionKey } from "../sends/permissions.js";
6
6
  import { type ConfigIssue } from "./config-issues.js";
7
+ import type { CommandPermissions } from "./permission-keys.js";
7
8
  export declare const plain: (rule: string) => (issue: v.BaseIssue<unknown>) => string;
8
9
  export declare const count: v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>;
9
10
  export declare const flag: v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>;
@@ -24,6 +25,11 @@ export interface SettingsExtension<T extends object = Record<never, never>> {
24
25
  defaults?: v.ObjectEntries;
25
26
  /** Profiles this messenger keeps in files of its own, beyond the config file and the remembered accounts. */
26
27
  profiles?: (env: NodeJS.ProcessEnv) => string[];
28
+ /**
29
+ * A CLI that is not a messenger declares its own permission keys (`commandPermissions`): `permissions` and
30
+ * `--permission` then take any command path, which the program checks against its commands.
31
+ */
32
+ permissions?: CommandPermissions;
27
33
  }
28
34
  export interface SettingsExtensionContext {
29
35
  flags: GlobalFlags;
@@ -272,7 +278,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
272
278
  record: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
273
279
  record_input: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
274
280
  keep_runs_for_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
275
- permissions: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, (issue: v.RegexIssue<string>) => string>]>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
281
+ permissions: v.OptionalSchema<v.RecordSchema<v.GenericSchema<string>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
276
282
  sends_per_hour: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
277
283
  requests_per_minute: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 0, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
278
284
  transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
@@ -346,7 +352,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
346
352
  record: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
347
353
  record_input: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
348
354
  keep_runs_for_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
349
- permissions: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, (issue: v.RegexIssue<string>) => string>]>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
355
+ permissions: v.OptionalSchema<v.RecordSchema<v.GenericSchema<string>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
350
356
  sends_per_hour: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
351
357
  requests_per_minute: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 0, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
352
358
  transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
@@ -364,6 +370,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
364
370
  readonly profiles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StrictObjectSchema<v.ObjectEntries, (issue: v.StrictObjectIssue) => string>, "has to be an object of profiles, by name">, undefined>;
365
371
  }, (issue: v.StrictObjectIssue) => string>, undefined>;
366
372
  }, (issue: v.StrictObjectIssue) => string>;
373
+ permissions?: CommandPermissions | undefined;
367
374
  };
368
375
  /** What `settingsFor` hands a CLI — named, so a CLI can export it without naming the schema library's types. */
369
376
  export type Configuration = ReturnType<typeof settingsFor>;
@@ -5,7 +5,7 @@ import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
5
5
  import { parseDuration } from "../domain/duration.js";
6
6
  import { DEFAULT_RERANK_MODEL, RERANK_MODELS } from "../embeddings/models.js";
7
7
  import { envName, readOnlyLockOf } from "../ports/app.js";
8
- import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
8
+ import { assertOverridesTighten, CEILING_KEY, COMMAND_PATH_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCE_KEY, RESOURCES, } from "../sends/permissions.js";
9
9
  import { storePath } from "../store/path.js";
10
10
  import { sharedConfigPath } from "../store/shared-config.js";
11
11
  import { checkConfigText, issueMessage, readConfigFile, readConfigText, savedPermissionKeys, schemaIssues, settingPathOf, withoutPath, } from "./config-issues.js";
@@ -39,7 +39,9 @@ const wholeOrZero = (text, name) => {
39
39
  throw new CliError("validation_error", `${name} has to be a whole number, 0 or more`);
40
40
  return value;
41
41
  };
42
- const permissionLevels = v.record(v.pipe(v.string(), v.regex(new RegExp(`^(${RESOURCES.join("|")})(\\.[a-z][a-z-]*)*$`), (issue) => `${issue.input} is not a command path — it starts with one of ${RESOURCES.join(", ")}`)), v.picklist(LEVELS, (issue) => `has to be one of ${LEVELS.join(", ")}, not ${issue.received}`), plain('has to be an object of command paths and levels, like {"messages.delete": "ask"}'));
42
+ const levelsOf = (key, example) => v.record(key, v.picklist(LEVELS, (issue) => `has to be one of ${LEVELS.join(", ")}, not ${issue.received}`), plain(`has to be an object of command paths and levels, like {"${example}": "ask"}`));
43
+ const permissionLevels = levelsOf(v.pipe(v.string(), v.regex(RESOURCE_KEY, (issue) => `${issue.input} is not a command path — it starts with one of ${RESOURCES.join(", ")}`)), "messages.delete");
44
+ const commandPathLevels = levelsOf(v.pipe(v.string(), v.regex(COMMAND_PATH_KEY, (issue) => `${issue.input} is not a command path, like api or events.delete`)), "events.delete");
43
45
  /** valibot's own words ("Expected never but received …") mean nothing to someone editing a file. */
44
46
  const objectMessage = (issue) => issue.expected === "never" ? "unknown setting" : "has to be an object, in braces";
45
47
  /**
@@ -122,7 +124,12 @@ export const fromFile = (settings, key, fallback) => first([
122
124
  * here; a command that re-derived the order would disagree with the others.
123
125
  */
124
126
  export const settingsFor = (app, extension = {}) => {
125
- const profileEntries = { ...SHARED_PROFILE_ENTRIES, ...extension.profile };
127
+ const declared = extension.permissions;
128
+ const profileEntries = {
129
+ ...SHARED_PROFILE_ENTRIES,
130
+ ...(declared ? { permissions: v.optional(commandPathLevels) } : {}),
131
+ ...extension.profile,
132
+ };
126
133
  const defaultsOnly = { ...SHARED_DEFAULTS_ONLY, ...extension.defaults };
127
134
  const defaultsEntries = { ...profileEntries, ...defaultsOnly };
128
135
  const strictOf = (entries) => v.strictObject(entries, objectMessage);
@@ -252,8 +259,8 @@ export const settingsFor = (app, extension = {}) => {
252
259
  from,
253
260
  scope?.permissions,
254
261
  ]);
255
- const overrides = permissionOverrides(flags.permission);
256
- assertOverridesTighten(overrides, layerPermissions(fileLayers).levels);
262
+ const overrides = permissionOverrides(flags.permission, declared ? COMMAND_PATH_KEY : RESOURCE_KEY);
263
+ assertOverridesTighten(overrides, layerPermissions(fileLayers).levels, declared?.defaults);
257
264
  const permissions = withCeiling(layerPermissions([["flag", overrides], ...fileLayers]), env);
258
265
  const updateCheck = first([
259
266
  [
@@ -408,7 +415,15 @@ export const settingsFor = (app, extension = {}) => {
408
415
  saveConfigFile(path, checked.success ? checked.output : changed);
409
416
  return valueAt(checked.success ? checked.output : changed, at) ?? null;
410
417
  };
411
- return { resolveSettings, configuredProfiles, changeSetting, allSettings, defaultsOnlyKeys, schema };
418
+ return {
419
+ resolveSettings,
420
+ configuredProfiles,
421
+ changeSetting,
422
+ allSettings,
423
+ defaultsOnlyKeys,
424
+ schema,
425
+ ...(declared ? { permissions: declared } : {}),
426
+ };
412
427
  };
413
428
  const isScope = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
414
429
  const valueAt = (node, path) => path.reduce((at, key) => (isScope(at) || Array.isArray(at) ? at[key] : undefined), node);
@@ -81,7 +81,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
81
81
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
82
82
  record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
83
83
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
84
- permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
84
+ permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").GenericSchema<string>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
85
85
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
86
86
  requests_per_minute: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 0, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
87
87
  transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
@@ -155,7 +155,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
155
155
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
156
156
  record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
157
157
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
158
- permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
158
+ permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").GenericSchema<string>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
159
159
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
160
160
  requests_per_minute: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 0, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
161
161
  transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
@@ -173,8 +173,9 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
173
173
  readonly profiles: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").StringSchema<undefined>, import("valibot").StrictObjectSchema<import("valibot").ObjectEntries, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, undefined>;
174
174
  }, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
175
175
  }, (issue: import("valibot").StrictObjectIssue) => string>;
176
+ permissions?: import("./permission-keys.js").CommandPermissions | undefined;
176
177
  }, args_2?: {
177
- permissionKey?: import("./permission-keys.js").PermissionKeyOf;
178
+ permissionKey?: import("./permission-keys.js").PermissionKeySource | undefined;
178
179
  } | undefined) => import("commander").Command;
179
180
  export declare const guideCommand: (app: Pick<import("./entry.js").AppIdentity, "command">, options: import("./guide.js").GuideOptions) => import("commander").Command;
180
181
  export declare const addMeetingBatchCommand: (program: import("commander").Command, deps: {
@@ -284,7 +285,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
284
285
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
285
286
  record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
286
287
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
287
- permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
288
+ permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").GenericSchema<string>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
288
289
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
289
290
  requests_per_minute: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 0, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
290
291
  transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
@@ -358,7 +359,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
358
359
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
359
360
  record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
360
361
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
361
- permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
362
+ permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").GenericSchema<string>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
362
363
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
363
364
  requests_per_minute: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 0, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
364
365
  transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
@@ -376,6 +377,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
376
377
  readonly profiles: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").StringSchema<undefined>, import("valibot").StrictObjectSchema<import("valibot").ObjectEntries, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, undefined>;
377
378
  }, (issue: import("valibot").StrictObjectIssue) => string>, undefined>;
378
379
  }, (issue: import("valibot").StrictObjectIssue) => string>;
380
+ permissions?: import("./permission-keys.js").CommandPermissions | undefined;
379
381
  }, "configuredProfiles">, options?: import("./messenger/complete-command.js").CompletionOptions | undefined) => import("commander").Command;
380
382
  export declare const contactsCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
381
383
  export declare const conversationsCommand: (messenger: import("./index.js").Messenger) => import("commander").Command;
@@ -2,9 +2,13 @@
2
2
  * The tunnel's address a public listener is reached at, checked one way for every command that opens
3
3
  * one. `missing` says what needs it; `reaches` who calls through it, for the https refusal.
4
4
  */
5
- export declare const publicUrlOf: (given: string | undefined, { missing, reaches }: {
5
+ export declare const publicUrlOf: (given: string | undefined, { missing, reaches, path, }: {
6
6
  missing: string;
7
7
  reaches: string;
8
+ /** An MCP server may sit under a path of a shared address, `https://<name>.ts.net/tg`, one per tool. */
9
+ path?: boolean;
8
10
  }) => URL;
9
11
  /** A local port from `--port`, or the command's own default. */
10
12
  export declare const portOf: (given: string | undefined, fallback: number) => number;
13
+ /** The connector address under a public address: `https://<name>.ts.net/mcp`, or `…/tg/mcp` under a path. */
14
+ export declare const mcpUrlOf: (publicUrl: URL) => URL;
@@ -4,7 +4,7 @@ const EXAMPLE = "--public-url https://<name>.ts.net";
4
4
  * The tunnel's address a public listener is reached at, checked one way for every command that opens
5
5
  * one. `missing` says what needs it; `reaches` who calls through it, for the https refusal.
6
6
  */
7
- export const publicUrlOf = (given, { missing, reaches }) => {
7
+ export const publicUrlOf = (given, { missing, reaches, path = false, }) => {
8
8
  if (!given)
9
9
  throw new CliError("configuration_error", `${missing} needs the tunnel's address: ${EXAMPLE}`);
10
10
  let url;
@@ -19,8 +19,14 @@ export const publicUrlOf = (given, { missing, reaches }) => {
19
19
  const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
20
20
  if (url.protocol !== "https:" && !local)
21
21
  throw new CliError("validation_error", `--public-url must be https — ${reaches} through it`);
22
- if (url.pathname !== "/" || url.search || url.hash)
23
- throw new CliError("validation_error", `--public-url is the tunnel's address only, without a path: ${EXAMPLE}`);
22
+ if (url.search || url.hash || (!path && url.pathname !== "/"))
23
+ throw new CliError("validation_error", path
24
+ ? `--public-url is the tunnel's address, or a path of it, without a query: ${EXAMPLE}/tg`
25
+ : `--public-url is the tunnel's address only, without a path: ${EXAMPLE}`);
26
+ if (path && !/^(\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*\/?$/.test(url.pathname))
27
+ throw new CliError("validation_error", "--public-url's path takes letters, digits, dot, dash and underscore: /tg");
28
+ if (path)
29
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
24
30
  return url;
25
31
  };
26
32
  /** A local port from `--port`, or the command's own default. */
@@ -32,3 +38,5 @@ export const portOf = (given, fallback) => {
32
38
  throw new CliError("validation_error", "--port takes 1–65535");
33
39
  return port;
34
40
  };
41
+ /** The connector address under a public address: `https://<name>.ts.net/mcp`, or `…/tg/mcp` under a path. */
42
+ export const mcpUrlOf = (publicUrl) => new URL(`${publicUrl.pathname.replace(/\/+$/, "")}/mcp`, publicUrl.origin);
@@ -2,6 +2,7 @@ import { type CommandInfo, type CommandOutput } from "@wirecat/cli-base/commands
2
2
  import { type ErrorObject } from "@wirecat/cli-base/standards";
3
3
  import * as v from "valibot";
4
4
  import { type CommandAvailability } from "../cli/command-availability.js";
5
+ import type { CommandPermissions } from "../cli/permission-keys.js";
5
6
  export type { CommandAvailability } from "../cli/command-availability.js";
6
7
  import type { RunOptions } from "../cli/program.js";
7
8
  import type { StdinField } from "../cli/stdin-field.js";
@@ -22,6 +23,8 @@ export interface AgentContext {
22
23
  overrides?: readonly string[];
23
24
  /** The key a CLI's own command is checked against, when `keyForCommand` does not know it. */
24
25
  keyOf?: (path: readonly string[]) => PermissionKey | null | undefined;
26
+ /** A declared CLI's keys and defaults (`configuration.permissions`); in place of `keyOf`. */
27
+ permissions?: CommandPermissions | undefined;
25
28
  authority?: CatalogueOptions["authority"];
26
29
  /** The server's one open connection. The command borrows it: it cannot close it, and never opens its own. */
27
30
  connection?: () => Promise<MessengerAdapter>;
@@ -60,6 +63,8 @@ export interface CatalogueOptions {
60
63
  /** Whether a read-only lock currently blocks messenger changes. */
61
64
  readOnly?: boolean;
62
65
  keyOf?: (path: readonly string[]) => PermissionKey | null | undefined;
66
+ /** A declared CLI's keys and defaults (`configuration.permissions`); in place of `keyOf`. */
67
+ permissions?: CommandPermissions | undefined;
63
68
  /** Whether a command changes what a later run may do (`grantsAuthority`): execution requires `mcp.authority`. */
64
69
  authority?: (path: readonly string[]) => boolean;
65
70
  /** Arguments left out of every tool, by name. */
package/dist/mcp/agent.js CHANGED
@@ -124,7 +124,7 @@ const schemaOf = (fields) => v.object(Object.fromEntries(fields.map((field) => [
124
124
  const startsWith = (path, prefix) => prefix.every((word, at) => path[at] === word);
125
125
  /** Build the discoverable command catalogue; profile permissions annotate availability. */
126
126
  export const toolsFromCommands = (commands, options = {}) => {
127
- const { root = [], exclude = [], levels, keyOf, authority, readOnly = false } = options;
127
+ const { root = [], exclude = [], levels, keyOf, authority, readOnly = false, permissions } = options;
128
128
  return flatten(commands)
129
129
  .filter((info) =>
130
130
  // A group is offered only when it runs on its own; without an action it only prints its help.
@@ -136,9 +136,11 @@ export const toolsFromCommands = (commands, options = {}) => {
136
136
  info.lifetime === undefined &&
137
137
  (options.includeUnavailable === true ||
138
138
  (!(readOnly && (changesMessenger(info) || authority?.(info.path))) &&
139
- (levels === undefined || commandAvailability(info, levels, keyOf, authority).available))))
139
+ (levels === undefined && !(readOnly && permissions)
140
+ ? true
141
+ : commandAvailability(info, levels ?? {}, keyOf, authority, readOnly, true, permissions).available))))
140
142
  .map((info) => ({
141
- availability: () => commandAvailability(info, options.currentLevels?.() ?? levels ?? {}, keyOf, authority, readOnly),
143
+ availability: () => commandAvailability(info, options.currentLevels?.() ?? levels ?? {}, keyOf, authority, readOnly, true, permissions),
142
144
  command: info.path.slice(root.length).join(" "),
143
145
  path: info.path,
144
146
  title: info.summary ?? info.description,
@@ -189,7 +191,8 @@ export const runCommandForAgent = async (program, path, input, ctx) => {
189
191
  if (info.lifetime)
190
192
  throw new CliError("validation_error", `"${path.join(" ")}" runs until it is stopped; an agent call must return`);
191
193
  const levels = ctx.levels?.();
192
- const availability = (levels || ctx.readOnly) && commandAvailability(info, levels ?? {}, ctx.keyOf, ctx.authority, ctx.readOnly);
194
+ const availability = (levels || ctx.readOnly || ctx.permissions) &&
195
+ commandAvailability(info, levels ?? {}, ctx.keyOf, ctx.authority, ctx.readOnly, true, ctx.permissions);
193
196
  if (availability && !availability.available)
194
197
  throw new CliError("permission_error", `${path.join(" ")}: ${availability.reason}`, {
195
198
  sent: false,
@@ -0,0 +1,47 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import { McpServer, type Tool } from "@modelcontextprotocol/server";
3
+ import type { HttpOptions } from "./http/serve.js";
4
+ export declare const CHILD_IDLE_MS: number;
5
+ /** How to start the one-profile server for a profile: `<node> <script> <profile> mcp`. */
6
+ export interface ChildSpec {
7
+ command: string;
8
+ args: string[];
9
+ env: NodeJS.ProcessEnv;
10
+ }
11
+ export interface AllProfilesOptions {
12
+ /** `tg`: the tools' prefix and the server's name. */
13
+ command: string;
14
+ version: string;
15
+ /** `Telegram`: what the `profile` field says it picks. */
16
+ name: string;
17
+ /** Read again for each new session, so a profile set up while serving over HTTP shows up. */
18
+ profiles: () => readonly string[];
19
+ child: (profile: string) => ChildSpec;
20
+ instructions: string;
21
+ idleMs?: number;
22
+ /** A child's stderr, line by line; never this process's stdout, which carries the protocol. */
23
+ note?: (line: string) => void;
24
+ }
25
+ /** The child's tool with the one field that picks the profile, required and without a default. */
26
+ export declare const withProfile: (tool: Tool, profiles: readonly string[], name: string) => Tool;
27
+ /**
28
+ * **One server for every profile of a tool** (TASK-26): `<tool> mcp` shows the tools `<tool> <profile> mcp` shows,
29
+ * each with a required `profile`, and runs a call in that profile's own server, started as a child the first time
30
+ * it is needed. The child applies the profile's permissions, keyring, store and audit log, so nothing here decides
31
+ * what a profile may do; this only routes.
32
+ */
33
+ export declare const allProfilesServer: (options: AllProfilesOptions) => {
34
+ build: () => McpServer;
35
+ close: () => Promise<void>;
36
+ };
37
+ /** Over stdin and stdout until the client closes stdin or the process is told to stop; then the children close. */
38
+ export declare const serveAllProfilesOverStdio: (options: AllProfilesOptions, io?: {
39
+ stdin?: Readable;
40
+ stdout?: Writable;
41
+ signal?: AbortSignal;
42
+ }) => Promise<void>;
43
+ /** Over HTTP behind the tunnel, one login for every profile; the caller keeps it running and closes it. */
44
+ export declare const serveAllProfilesOverHttp: (options: AllProfilesOptions, http: Omit<HttpOptions, "appName" | "onError">) => Promise<{
45
+ url: URL;
46
+ close: () => Promise<void>;
47
+ }>;