@moxxy/sdk 0.32.0 → 0.34.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 (94) hide show
  1. package/README.md +1 -1
  2. package/dist/audit-export.d.ts +51 -0
  3. package/dist/audit-export.d.ts.map +1 -0
  4. package/dist/audit-export.js +4 -0
  5. package/dist/audit-export.js.map +1 -0
  6. package/dist/audit.d.ts +86 -0
  7. package/dist/audit.d.ts.map +1 -0
  8. package/dist/audit.js +35 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/define.d.ts +2 -0
  11. package/dist/define.d.ts.map +1 -1
  12. package/dist/define.js +1 -0
  13. package/dist/define.js.map +1 -1
  14. package/dist/egress.d.ts +109 -0
  15. package/dist/egress.d.ts.map +1 -0
  16. package/dist/egress.js +228 -0
  17. package/dist/egress.js.map +1 -0
  18. package/dist/events.d.ts +18 -0
  19. package/dist/events.d.ts.map +1 -1
  20. package/dist/fs-utils.d.ts +48 -0
  21. package/dist/fs-utils.d.ts.map +1 -1
  22. package/dist/fs-utils.js +114 -1
  23. package/dist/fs-utils.js.map +1 -1
  24. package/dist/hooks.d.ts +8 -0
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/mode/react-loop.d.ts.map +1 -1
  31. package/dist/mode/react-loop.js +10 -1
  32. package/dist/mode/react-loop.js.map +1 -1
  33. package/dist/plugin.d.ts +10 -0
  34. package/dist/plugin.d.ts.map +1 -1
  35. package/dist/principal-os.d.ts +14 -0
  36. package/dist/principal-os.d.ts.map +1 -0
  37. package/dist/principal-os.js +48 -0
  38. package/dist/principal-os.js.map +1 -0
  39. package/dist/principal.d.ts +54 -0
  40. package/dist/principal.d.ts.map +1 -0
  41. package/dist/principal.js +18 -0
  42. package/dist/principal.js.map +1 -0
  43. package/dist/redact.d.ts +36 -0
  44. package/dist/redact.d.ts.map +1 -0
  45. package/dist/redact.js +88 -0
  46. package/dist/redact.js.map +1 -0
  47. package/dist/schemas.d.ts +18 -18
  48. package/dist/secret-provider.d.ts +60 -0
  49. package/dist/secret-provider.d.ts.map +1 -0
  50. package/dist/secret-provider.js +59 -0
  51. package/dist/secret-provider.js.map +1 -0
  52. package/dist/server.d.ts +3 -1
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +6 -1
  55. package/dist/server.js.map +1 -1
  56. package/dist/session-like.d.ts +3 -0
  57. package/dist/session-like.d.ts.map +1 -1
  58. package/dist/signature.d.ts +21 -0
  59. package/dist/signature.d.ts.map +1 -0
  60. package/dist/signature.js +32 -0
  61. package/dist/signature.js.map +1 -0
  62. package/dist/tool-icon.d.ts +22 -0
  63. package/dist/tool-icon.d.ts.map +1 -0
  64. package/dist/tool-icon.js +44 -0
  65. package/dist/tool-icon.js.map +1 -0
  66. package/dist/tool.d.ts +8 -0
  67. package/dist/tool.d.ts.map +1 -1
  68. package/package.json +9 -2
  69. package/src/audit-export.ts +57 -0
  70. package/src/audit.ts +137 -0
  71. package/src/cross-process-lock.test.ts +23 -14
  72. package/src/define.ts +3 -0
  73. package/src/egress.test.ts +238 -0
  74. package/src/egress.ts +274 -0
  75. package/src/events.ts +18 -0
  76. package/src/fs-utils.test.ts +98 -2
  77. package/src/fs-utils.ts +120 -1
  78. package/src/hooks.ts +8 -0
  79. package/src/host-credential.test.ts +39 -0
  80. package/src/index.ts +49 -0
  81. package/src/mode/react-loop.ts +10 -1
  82. package/src/plugin.ts +10 -0
  83. package/src/principal-os.ts +48 -0
  84. package/src/principal.test.ts +47 -0
  85. package/src/principal.ts +60 -0
  86. package/src/redact.test.ts +91 -0
  87. package/src/redact.ts +91 -0
  88. package/src/secret-provider.ts +84 -0
  89. package/src/server.ts +17 -0
  90. package/src/session-like.ts +3 -0
  91. package/src/signature.ts +34 -0
  92. package/src/tool-icon.test.ts +46 -0
  93. package/src/tool-icon.ts +46 -0
  94. package/src/tool.ts +8 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Where a named secret comes from: a swappable block, like every other.
3
+ *
4
+ * The built-in vault (AES-256-GCM under `~/.moxxy/vault.json`, unlocked via the
5
+ * OS keychain) is a good design for one machine and unusable for a fleet: there
6
+ * is no central issuance, no rotation, and no way to revoke a single
7
+ * workstation. Every organisation already runs something that does those three
8
+ * things, so the answer is to let them plug it in rather than to reimplement it.
9
+ *
10
+ * Deliberately ONE block rather than two. A host-scoped credential (a token for
11
+ * an internal registry or a GitHub Enterprise host) is a named secret with a
12
+ * naming convention on top; splitting it into a second `CredentialProvider`
13
+ * registry would leave two overlapping abstractions to keep in sync for no
14
+ * gain.
15
+ */
16
+
17
+ export interface SecretProviderScope {
18
+ readonly cwd: string;
19
+ }
20
+
21
+ export interface SecretProviderSession {
22
+ /**
23
+ * Resolve a secret by name, or null when this provider does not hold it.
24
+ *
25
+ * Returning null rather than throwing is what makes providers composable: the
26
+ * host tries the active provider and falls back to the local vault, so
27
+ * adopting an external store does not require migrating every secret on day
28
+ * one.
29
+ *
30
+ * MUST NOT throw for an ordinary miss. A genuine failure (the store is
31
+ * unreachable, the token expired) SHOULD throw, because silently reporting
32
+ * "no such secret" for an outage would send the caller down a
33
+ * missing-credential path and hide the real cause.
34
+ */
35
+ get(name: string): Promise<string | null>;
36
+ close(): Promise<void>;
37
+ }
38
+
39
+ export interface SecretProviderDef {
40
+ readonly name: string;
41
+ readonly description?: string;
42
+ open(scope: SecretProviderScope): SecretProviderSession;
43
+ }
44
+
45
+ /** Freeze a secret-provider spec, mirroring the other `defineX` factories. */
46
+ export function defineSecretProvider(def: SecretProviderDef): SecretProviderDef {
47
+ return Object.freeze({ ...def });
48
+ }
49
+
50
+ /**
51
+ * The canonical secret name holding a credential for `host`.
52
+ *
53
+ * A host credential is a named secret plus a convention, which is exactly why
54
+ * there is no separate `CredentialProvider` registry: whatever store an
55
+ * organisation already plugged in for secrets serves these too.
56
+ *
57
+ * `github.example.com` becomes `MOXXY_CREDENTIAL_GITHUB_EXAMPLE_COM`. The
58
+ * derivation mirrors the provider-key convention (upper-snake, non-alphanumerics
59
+ * to `_`) so an operator who has seen one can predict the other, and a port is
60
+ * dropped because a credential belongs to the host, not to a socket.
61
+ *
62
+ * Accepts a bare host or a full URL; anything unparseable yields null rather
63
+ * than a name derived from garbage, so a caller cannot accidentally look up a
64
+ * secret keyed on a typo.
65
+ */
66
+ export function hostCredentialName(hostOrUrl: string): string | null {
67
+ const raw = hostOrUrl.trim();
68
+ if (raw.length === 0) return null;
69
+ let host = raw;
70
+ if (raw.includes('://')) {
71
+ try {
72
+ host = new URL(raw).hostname;
73
+ } catch {
74
+ return null;
75
+ }
76
+ } else {
77
+ // Strip a port or a path if the caller passed `host:443/x`.
78
+ host = raw.split('/')[0]!.split(':')[0]!;
79
+ }
80
+ host = host.replace(/^\[|\]$/g, '').trim();
81
+ if (host.length === 0 || /\s/.test(host)) return null;
82
+ const slug = host.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
83
+ return slug.length > 0 ? `MOXXY_CREDENTIAL_${slug}` : null;
84
+ }
package/src/server.ts CHANGED
@@ -20,7 +20,24 @@ export {
20
20
  writeFileAtomicSync,
21
21
  moxxyHome,
22
22
  moxxyPath,
23
+ ensurePrivateDir,
24
+ ensurePrivateFile,
25
+ pruneStaleTempFiles,
26
+ PRIVATE_DIR_MODE,
27
+ PRIVATE_FILE_MODE,
23
28
  } from './fs-utils.js';
29
+ // The local OS account as a Principal (node:os). The type and its pure helpers
30
+ // ride the main barrel.
31
+ export { resolveOsPrincipal } from './principal-os.js';
32
+ // Outbound proxy support for global `fetch` (dynamic `undici` import). Node-only.
33
+ export {
34
+ installEgressProxy,
35
+ readProxyEnv,
36
+ parseNoProxy,
37
+ shouldBypassProxy,
38
+ redactProxyUrl,
39
+ hasProxy,
40
+ } from './egress.js';
24
41
  // Cross-process "fire exactly once" lock (node:fs). Value lives here; its
25
42
  // options type is re-exported from the main barrel like other erased types.
26
43
  export { CrossProcessFireLock } from './cross-process-lock.js';
@@ -6,6 +6,7 @@ import type { ApprovalResolver, ModeBadge } from './mode.js';
6
6
  import type { PermissionResolver } from './permission.js';
7
7
  import type { ModelDescriptor, ProviderKeyValidation } from './provider.js';
8
8
  import type { PluginSetupSpec } from './schemas.js';
9
+ import type { ToolIcon } from './tool-icon.js';
9
10
  import type { ToolCompactPresentation } from './tool.js';
10
11
 
11
12
  /**
@@ -80,6 +81,8 @@ export interface ToolInfo {
80
81
  readonly description: string;
81
82
  /** Compact presentation hint (plain data - crosses the wire intact). */
82
83
  readonly compact?: ToolCompactPresentation;
84
+ /** Declared icon, so a surface renders the tool's own choice, not a guess. */
85
+ readonly icon?: ToolIcon;
83
86
  }
84
87
 
85
88
  /** Serializable skill metadata. */
@@ -0,0 +1,34 @@
1
+ import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
2
+
3
+ /**
4
+ * Ed25519 detached-signature verification over exact bytes.
5
+ *
6
+ * Lives in the SDK rather than beside its first caller because two different
7
+ * things now depend on it, and one of them is policy. Policy has to bind on a
8
+ * machine with no plugins installed at all, so the verifier it uses cannot sit
9
+ * inside a plugin: a control that a user can disable by uninstalling something
10
+ * is not a control.
11
+ *
12
+ * Verifies over the bytes as received, never over a re-serialisation of the
13
+ * parsed object. Canonicalising first would let two different documents share a
14
+ * signature whenever the parser and the serialiser disagree about anything,
15
+ * which is how signature-verification bypasses usually happen.
16
+ *
17
+ * Returns false rather than throwing on a malformed key or signature: to a
18
+ * caller "this did not verify" and "this could not be parsed" warrant the same
19
+ * response, and a throw here would tempt a try/catch that swallows both into a
20
+ * success path.
21
+ */
22
+ export function verifyEd25519(
23
+ bytes: Uint8Array,
24
+ signatureB64: string,
25
+ publicKeyPem: string,
26
+ ): boolean {
27
+ if (!publicKeyPem || !signatureB64) return false;
28
+ try {
29
+ const key = createPublicKey(publicKeyPem);
30
+ return cryptoVerify(null, Buffer.from(bytes), key, Buffer.from(signatureB64, 'base64'));
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { TOOL_ICONS, isToolIcon } from './tool-icon.js';
3
+ import { defineTool } from './define.js';
4
+ import { z } from 'zod';
5
+
6
+ describe('tool icons', () => {
7
+ it('is a closed vocabulary, so a surface can map it exhaustively', () => {
8
+ expect(isToolIcon('terminal')).toBe(true);
9
+ // The whole reason the field is not a free string: a name no surface owns
10
+ // would render as nothing anywhere.
11
+ expect(isToolIcon('sparkles-v2')).toBe(false);
12
+ expect(isToolIcon('')).toBe(false);
13
+ expect(isToolIcon(undefined)).toBe(false);
14
+ });
15
+
16
+ it('has no duplicates, which would make a surface map ambiguous', () => {
17
+ expect(new Set(TOOL_ICONS).size).toBe(TOOL_ICONS.length);
18
+ });
19
+
20
+ it('always offers a neutral choice for a tool that fits nothing else', () => {
21
+ expect(TOOL_ICONS).toContain('wrench');
22
+ });
23
+
24
+ it('carries a declared icon through defineTool onto the def', () => {
25
+ const tool = defineTool({
26
+ name: 'thing',
27
+ description: 'does a thing',
28
+ icon: 'workflow',
29
+ inputSchema: z.object({}),
30
+ handler: () => undefined,
31
+ });
32
+
33
+ expect(tool.icon).toBe('workflow');
34
+ });
35
+
36
+ it('leaves icon undefined when a tool does not declare one', () => {
37
+ const tool = defineTool({
38
+ name: 'thing',
39
+ description: 'does a thing',
40
+ inputSchema: z.object({}),
41
+ handler: () => undefined,
42
+ });
43
+
44
+ expect(tool.icon).toBeUndefined();
45
+ });
46
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * What a tool looks like, as a semantic name rather than a glyph.
3
+ *
4
+ * A CLOSED vocabulary on purpose. Surfaces render wildly differently: the
5
+ * desktop draws SVG, the TUI prints a character, a future web surface might do
6
+ * something else again. If this were a free string, a plugin could name an icon
7
+ * no surface owns and every surface would have to guess, so in practice
8
+ * everything would fall back and the field would be decorative. A fixed set
9
+ * means each surface maps it exhaustively and a plugin's choice actually
10
+ * survives to the screen.
11
+ *
12
+ * The names describe WHAT THE TOOL TOUCHES, not what it looks like, so a
13
+ * surface is free to change its artwork without every plugin becoming wrong.
14
+ *
15
+ * Adding a member is a breaking change for surfaces (their maps stop being
16
+ * exhaustive and stop compiling), which is the intended cost: a new icon should
17
+ * be a deliberate decision across surfaces, not a silent fallback to a wrench.
18
+ */
19
+ export const TOOL_ICONS = [
20
+ 'file',
21
+ 'folder',
22
+ 'search',
23
+ 'edit',
24
+ 'diff',
25
+ 'terminal',
26
+ 'globe',
27
+ 'chat',
28
+ 'workflow',
29
+ 'agent',
30
+ 'settings',
31
+ 'lock',
32
+ 'plug',
33
+ 'mic',
34
+ 'speaker',
35
+ 'smartphone',
36
+ 'workspace',
37
+ 'clipboard',
38
+ 'spark',
39
+ 'wrench',
40
+ ] as const;
41
+
42
+ export type ToolIcon = (typeof TOOL_ICONS)[number];
43
+
44
+ export function isToolIcon(value: unknown): value is ToolIcon {
45
+ return typeof value === 'string' && (TOOL_ICONS as ReadonlyArray<string>).includes(value);
46
+ }
package/src/tool.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  import type { EventLogReader } from './log.js';
3
+ import type { ToolIcon } from './tool-icon.js';
3
4
  import type { PermissionRule } from './permission.js';
4
5
  import type { SessionId, ToolCallId, TurnId } from './ids.js';
5
6
  import type { SubagentSpawner } from './subagent.js';
@@ -191,6 +192,13 @@ export interface ToolDef {
191
192
  readonly handler: (input: unknown, ctx: ToolContext) => Promise<unknown> | unknown;
192
193
  /** Opt-in presentation hint. See `ToolCompactPresentation`. */
193
194
  readonly compact?: ToolCompactPresentation;
195
+ /**
196
+ * What this tool touches, so surfaces can show it. Without this a surface can
197
+ * only guess from the NAME, which works for the handful of built-ins it was
198
+ * written against and gives every plugin-contributed tool the same generic
199
+ * fallback. See {@link ToolIcon}.
200
+ */
201
+ readonly icon?: ToolIcon;
194
202
  /**
195
203
  * Optional capability declaration. Advisory unless the user enables
196
204
  * `@moxxy/plugin-security`, at which point the active `Isolator`