@rho-workspace/rho 0.5.4 → 0.5.6

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 (161) hide show
  1. package/dist/agent-connect.d.ts +8 -0
  2. package/dist/agent-connect.js +15 -0
  3. package/dist/browser-access.js +3 -2
  4. package/dist/capabilities.d.ts +2 -2
  5. package/dist/capabilities.js +6 -1
  6. package/dist/cli.js +219 -176
  7. package/dist/contract.generated.d.ts +472 -4
  8. package/dist/contract.generated.js +1 -1
  9. package/dist/contracts/owners.generated.js +455 -6
  10. package/dist/conversations.d.ts +157 -4
  11. package/dist/conversations.js +69 -6
  12. package/dist/discovery.d.ts +1 -0
  13. package/dist/discovery.js +3 -3
  14. package/dist/extension-contract.d.ts +99 -0
  15. package/dist/extension-contract.js +52 -0
  16. package/dist/extension-directory.d.ts +9 -0
  17. package/dist/extension-directory.js +11 -0
  18. package/dist/extension-worker.d.ts +1 -0
  19. package/dist/extension-worker.js +36 -0
  20. package/dist/extensions.d.ts +157 -0
  21. package/dist/extensions.js +349 -0
  22. package/dist/plugin-cli.d.ts +1 -0
  23. package/dist/plugin-cli.js +139 -0
  24. package/dist/plugin-inventory.json +18 -0
  25. package/dist/plugins.d.ts +3 -3
  26. package/dist/project-work.d.ts +3 -1
  27. package/dist/project-work.js +15 -2
  28. package/dist/protocol.d.ts +8 -0
  29. package/dist/protocol.js +16 -7
  30. package/dist/server.d.ts +5 -10
  31. package/dist/server.js +122 -12
  32. package/dist/web/.vite/manifest.json +67 -67
  33. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  34. package/dist/web/assets/choices-D-YfnFP6.js +1 -0
  35. package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
  36. package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
  37. package/dist/web/assets/dist-DpIHRk3B.js +1 -0
  38. package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
  39. package/dist/web/assets/dist-pF902jHA.js +3 -0
  40. package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
  41. package/dist/web/assets/graph-lENgfQNH.js +2 -0
  42. package/dist/web/assets/index-B3_OVy91.css +1 -0
  43. package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
  44. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
  45. package/dist/web/assets/overview-CDnVKlOt.js +6 -0
  46. package/dist/web/assets/preview-BEC1E9MK.js +1 -0
  47. package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
  48. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
  49. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
  50. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
  51. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
  52. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
  53. package/dist/web/assets/workspace-COmCbGV3.js +5 -0
  54. package/dist/web/composition.json +65 -65
  55. package/dist/web/index.html +3 -3
  56. package/dist/web/renderer.json +1 -1
  57. package/dist/web-assets.d.ts +2 -1
  58. package/dist/web-assets.js +3 -2
  59. package/dist/work-contract.js +2 -2
  60. package/docs/FRONTEND.md +26 -18
  61. package/docs/HEADLESS.md +38 -28
  62. package/docs/PLUGIN-AUTHORING.md +4 -0
  63. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  64. package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
  67. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  71. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
  76. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  77. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  78. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  79. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  80. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  81. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  82. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  83. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  84. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  85. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  86. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  87. package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
  88. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  89. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  90. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  91. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  92. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  93. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  94. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  95. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  96. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  97. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  98. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  99. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  100. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  101. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  102. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  103. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  104. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  105. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  106. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  107. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  108. package/node_modules/hono/dist/jsx/base.js +49 -32
  109. package/node_modules/hono/dist/jsx/components.js +2 -2
  110. package/node_modules/hono/dist/jsx/constants.js +2 -1
  111. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  112. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  113. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  114. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  115. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  116. package/node_modules/hono/dist/utils/cookie.js +2 -2
  117. package/node_modules/hono/dist/utils/mime.js +1 -1
  118. package/node_modules/hono/dist/utils/url.js +1 -1
  119. package/node_modules/hono/package.json +1 -1
  120. package/package.json +5 -3
  121. package/protocol/index.json +93 -33
  122. package/protocol/operations/agent.sessions.list.json +146 -0
  123. package/protocol/operations/agent.sessions.read.json +315 -0
  124. package/protocol/operations/conversation.configure.json +29 -0
  125. package/protocol/operations/conversation.decide.json +29 -0
  126. package/protocol/operations/conversation.list.json +29 -0
  127. package/protocol/operations/conversation.permission.json +29 -0
  128. package/protocol/operations/conversation.read.json +29 -0
  129. package/protocol/operations/conversation.restore.json +29 -0
  130. package/protocol/operations/conversation.send.json +29 -0
  131. package/protocol/operations/conversation.settings.check.json +92 -0
  132. package/protocol/operations/conversation.settings.read.json +92 -0
  133. package/protocol/operations/conversation.settings.update.json +92 -0
  134. package/protocol/operations/conversation.start.json +46 -1
  135. package/protocol/operations/conversation.stop.json +29 -0
  136. package/protocol/operations/conversation.trash.json +29 -0
  137. package/protocol/operations/extension.call.json +132 -0
  138. package/protocol/operations/extension.content.json +78 -0
  139. package/protocol/operations/extension.describe.json +195 -0
  140. package/protocol/operations/extension.install.json +198 -0
  141. package/protocol/operations/extension.list.json +123 -0
  142. package/protocol/operations/extension.manage.json +55 -0
  143. package/protocol/operations/extension.read.json +112 -0
  144. package/protocol/operations/observation.snapshot.json +29 -0
  145. package/protocol/operations/project.work.read.json +3 -2
  146. package/protocol/operations/session.grant.json +9 -0
  147. package/protocol/operations/session.instructions.json +43 -0
  148. package/scripts/third-party-acceptance.mjs +77 -0
  149. package/vendor/composition.json +83 -83
  150. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  151. package/web/theme.css +12 -0
  152. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  153. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  154. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  155. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  156. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  157. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  158. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  159. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  160. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  161. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
@@ -0,0 +1,36 @@
1
+ // A process boundary for crashes/timeouts, NOT an OS sandbox. An installed host
2
+ // has this user's native permissions. Never send it service credentials.
3
+ import { pathToFileURL } from 'node:url';
4
+ const pending = new Map();
5
+ let next = 0;
6
+ process.on('message', async (message) => {
7
+ if (message.type === 'reply') {
8
+ const waiter = pending.get(message.id);
9
+ pending.delete(message.id);
10
+ if (message.error)
11
+ waiter?.reject(new Error(message.error));
12
+ else
13
+ waiter?.resolve(message.value);
14
+ }
15
+ if (message.type !== 'start')
16
+ return;
17
+ try {
18
+ const plugin = await import(pathToFileURL(message.entry).href);
19
+ if (typeof plugin.run !== 'function')
20
+ throw new Error('The plugin host must export run(operation, input, context).');
21
+ const value = await plugin.run(message.operation, message.input, {
22
+ projectRoot: message.projectRoot,
23
+ invocationId: message.invocationId,
24
+ changeId: (label) => 'ext:' + message.invocationId + ':' + String(label).slice(0, 40),
25
+ call: (method, body) => new Promise((resolve, reject) => {
26
+ const id = ++next;
27
+ pending.set(id, { resolve, reject });
28
+ process.send?.({ type: 'call', id, method, body });
29
+ }),
30
+ });
31
+ process.send?.({ type: 'result', value }, () => process.exit(0));
32
+ }
33
+ catch (error) {
34
+ process.send?.({ type: 'error', error: error instanceof Error ? error.message : String(error) }, () => process.exit(1));
35
+ }
36
+ });
@@ -0,0 +1,157 @@
1
+ export declare const extensionLimits: {
2
+ maxPlugins: number;
3
+ maxVersions: number;
4
+ maxFiles: number;
5
+ maxPackageBytes: number;
6
+ maxContentBytes: number;
7
+ maxReceipts: number;
8
+ maxResultBytes: number;
9
+ maxRunning: number;
10
+ maxDelegations: number;
11
+ timeoutMs: number;
12
+ installTimeoutMs: number;
13
+ };
14
+ export interface ExtensionManifest {
15
+ apiVersion: 1;
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ host?: string;
20
+ operations: {
21
+ id: string;
22
+ title: string;
23
+ description: string;
24
+ effect: 'read' | 'native';
25
+ methods: string[];
26
+ inputSchema: Record<string, any>;
27
+ }[];
28
+ skills: {
29
+ id: string;
30
+ title: string;
31
+ file: string;
32
+ }[];
33
+ views: {
34
+ id: string;
35
+ title: string;
36
+ file: string;
37
+ }[];
38
+ }
39
+ export interface InstalledExtension {
40
+ id: string;
41
+ name: string;
42
+ version: string;
43
+ title: string;
44
+ description: string;
45
+ digest: string;
46
+ source: string;
47
+ sourceCommit?: string;
48
+ enabled: boolean;
49
+ installedAt: string;
50
+ manifest: ExtensionManifest;
51
+ }
52
+ interface Receipt {
53
+ invocationId: string;
54
+ caller: string;
55
+ projectId: string;
56
+ pluginId: string;
57
+ packageDigest: string;
58
+ operation: string;
59
+ requestDigest: string;
60
+ state: 'pending' | 'completed' | 'unknown';
61
+ startedAt: string;
62
+ endedAt?: string;
63
+ value?: unknown;
64
+ error?: string;
65
+ delegations?: {
66
+ method: string;
67
+ request: unknown;
68
+ state: 'pending' | 'completed' | 'unknown';
69
+ value?: unknown;
70
+ error?: string;
71
+ }[];
72
+ }
73
+ export declare class Extensions {
74
+ readonly directory: string;
75
+ readonly limits: typeof extensionLimits;
76
+ private installed;
77
+ private running;
78
+ private workers;
79
+ private receiptCount;
80
+ private mutation;
81
+ constructor(directory: string, limits?: Partial<typeof extensionLimits>);
82
+ init(): Promise<void>;
83
+ close(): void;
84
+ private serial;
85
+ list(): {
86
+ directory: {
87
+ id: string;
88
+ title: string;
89
+ version: string;
90
+ description: string;
91
+ source: string;
92
+ repository: string;
93
+ needs: string;
94
+ }[];
95
+ plugins: {
96
+ id: string;
97
+ name: string;
98
+ version: string;
99
+ title: string;
100
+ description: string;
101
+ digest: string;
102
+ source: string;
103
+ sourceCommit?: string;
104
+ enabled: boolean;
105
+ installedAt: string;
106
+ }[];
107
+ limits: {
108
+ maxPlugins: number;
109
+ maxVersions: number;
110
+ maxFiles: number;
111
+ maxPackageBytes: number;
112
+ maxContentBytes: number;
113
+ maxReceipts: number;
114
+ maxResultBytes: number;
115
+ maxRunning: number;
116
+ maxDelegations: number;
117
+ timeoutMs: number;
118
+ installTimeoutMs: number;
119
+ };
120
+ };
121
+ describe(id: string): InstalledExtension;
122
+ private selected;
123
+ private root;
124
+ private file;
125
+ private hash;
126
+ check(root: string): Promise<{
127
+ name: string;
128
+ version: string;
129
+ manifest: ExtensionManifest;
130
+ }>;
131
+ private verify;
132
+ content(id: string, expected: string, kind: 'skill' | 'view', entryId: string): Promise<{
133
+ pluginId: string;
134
+ packageDigest: string;
135
+ entryId: string;
136
+ kind: "skill" | "view";
137
+ title: string;
138
+ text: string;
139
+ }>;
140
+ install(source: string, replace?: string, expected?: string): Promise<InstalledExtension>;
141
+ manage(id: string, expected: string, action: 'enable' | 'disable' | 'remove'): Promise<{
142
+ pluginId: string;
143
+ action: "remove" | "enable" | "disable";
144
+ retained: string;
145
+ }>;
146
+ read(invocationId: string, caller: string, projectId: string): Promise<Receipt>;
147
+ call(b: {
148
+ pluginId: string;
149
+ expectedDigest: string;
150
+ operation: string;
151
+ input: any;
152
+ changeId: string;
153
+ projectId: string;
154
+ }, caller: string, projectRoot: string, dispatch: (method: string, body: any, effect: 'read' | 'native') => Promise<unknown>): Promise<Receipt>;
155
+ private execute;
156
+ }
157
+ export {};
@@ -0,0 +1,349 @@
1
+ import { extensionDirectory } from './extension-directory.js';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { fork, spawn } from 'node:child_process';
6
+ import { Ajv } from 'ajv';
7
+ import { atomicJson, canonical, digest, requireThat } from '@rho-workspace/plugin-contracts';
8
+ export const extensionLimits = { maxPlugins: 32, maxVersions: 128, maxFiles: 20000, maxPackageBytes: 128 * 1024 * 1024, maxContentBytes: 512 * 1024, maxReceipts: 2000, maxResultBytes: 1024 * 1024, maxRunning: 4, maxDelegations: 32, timeoutMs: 30000, installTimeoutMs: 180000 };
9
+ const ajv = new Ajv({ strict: false, allErrors: true });
10
+ const identifier = /^[a-z0-9][a-z0-9._-]{0,95}$/;
11
+ const relativeFile = (v) => typeof v === 'string' && v.length < 512 && !path.isAbsolute(v) && !v.includes('\\') && v.split('/').every(p => p !== '..' && p !== '.' && p !== '');
12
+ const boundedText = (v, maximum = 2048) => typeof v === 'string' && v.length > 0 && v.length <= maximum;
13
+ async function command(file, args, cwd, timeout) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = spawn(file, args, { cwd, env: { ...process.env, npm_config_ignore_scripts: 'true', GIT_TERMINAL_PROMPT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });
16
+ let output = '', killed = false;
17
+ const timer = setTimeout(() => { killed = true; child.kill(); }, timeout);
18
+ for (const stream of [child.stdout, child.stderr])
19
+ stream?.on('data', chunk => { output = (output + chunk).slice(-8192); });
20
+ child.on('error', error => { clearTimeout(timer); reject(error); });
21
+ child.on('exit', code => { clearTimeout(timer); code === 0 && !killed ? resolve(output) : reject(new Error(killed ? `Package staging exceeded installTimeoutMs (${timeout} ms). The selected package was kept.` : `${file} failed (${code}): ${output}`)); });
22
+ });
23
+ }
24
+ export class Extensions {
25
+ directory;
26
+ limits;
27
+ installed = [];
28
+ running = new Map();
29
+ workers = new Set();
30
+ receiptCount = 0;
31
+ mutation = Promise.resolve();
32
+ constructor(directory, limits = {}) {
33
+ this.directory = directory;
34
+ this.limits = { ...extensionLimits, ...limits };
35
+ for (const [key, value] of Object.entries(this.limits))
36
+ requireThat(Number.isSafeInteger(value) && value > 0, 'invalid', `Invalid extensions.${key}.`);
37
+ }
38
+ async init() {
39
+ await fs.mkdir(path.join(this.directory, 'versions'), { recursive: true });
40
+ await fs.mkdir(path.join(this.directory, 'receipts'), { recursive: true });
41
+ try {
42
+ this.installed = JSON.parse(await fs.readFile(path.join(this.directory, 'installed.json'), 'utf8'));
43
+ }
44
+ catch (e) {
45
+ if (e.code !== 'ENOENT')
46
+ throw e;
47
+ }
48
+ const receipts = await fs.readdir(path.join(this.directory, 'receipts'));
49
+ this.receiptCount = receipts.length;
50
+ for (const file of receipts) {
51
+ const receipt = JSON.parse(await fs.readFile(path.join(this.directory, 'receipts', file), 'utf8'));
52
+ if (receipt.state === 'pending')
53
+ await atomicJson(path.join(this.directory, 'receipts', file), { ...receipt, state: 'unknown', endedAt: new Date().toISOString(), error: 'The service ended before the plugin result was retained. Observe the original native operation; do not repeat it under a new identity.' });
54
+ }
55
+ }
56
+ close() { for (const child of this.workers)
57
+ child.kill(); }
58
+ serial(fn) { const next = this.mutation.then(fn, fn); this.mutation = next.catch(() => { }); return next; }
59
+ list() { return { directory: extensionDirectory, plugins: this.installed.map(({ manifest, ...info }) => info), limits: this.limits }; }
60
+ describe(id) { const value = this.installed.find(p => p.id === id); requireThat(value, 'not_found', 'Plugin is not installed.'); return structuredClone(value); }
61
+ selected(id, expectedDigest) { const value = this.describe(id); requireThat(value.digest === expectedDigest, 'stale', 'The selected plugin changed. Read its descriptor again.'); return value; }
62
+ root(p) { return path.join(this.directory, 'versions', p.digest, 'node_modules', p.name); }
63
+ async file(root, relative) {
64
+ requireThat(relativeFile(relative), 'invalid', 'Plugin entry must be a relative file within its package.');
65
+ const resolved = await fs.realpath(path.join(root, relative));
66
+ requireThat(resolved.startsWith(await fs.realpath(root) + path.sep) && (await fs.stat(resolved)).isFile(), 'scope', 'Plugin entry escapes its package or is not a file.');
67
+ return resolved;
68
+ }
69
+ async hash(root) {
70
+ root = await fs.realpath(root);
71
+ const hash = createHash('sha256');
72
+ let bytes = 0, files = 0;
73
+ const walk = async (dir) => {
74
+ for (const item of (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
75
+ requireThat(++files <= this.limits.maxFiles, 'limit', 'Plugin tree exceeds extensions.maxFiles; the selected package is kept.');
76
+ const target = path.join(dir, item.name), relative = path.relative(root, target);
77
+ if (item.isDirectory()) {
78
+ hash.update('dir:' + relative + '\0');
79
+ await walk(target);
80
+ }
81
+ else if (item.isSymbolicLink()) {
82
+ const resolved = await fs.realpath(target);
83
+ requireThat(resolved.startsWith(root + path.sep) && (await fs.stat(resolved)).isFile(), 'scope', 'Package links must resolve to files within the installed tree.');
84
+ hash.update('link:' + relative + '\0' + await fs.readlink(target) + '\0');
85
+ }
86
+ else {
87
+ requireThat(item.isFile(), 'invalid', 'Only regular files and contained file links are supported.');
88
+ bytes += (await fs.stat(target)).size;
89
+ requireThat(bytes <= this.limits.maxPackageBytes, 'limit', 'Plugin tree exceeds extensions.maxPackageBytes; the selected package is kept.');
90
+ hash.update('file:' + relative + '\0');
91
+ hash.update(await fs.readFile(target));
92
+ hash.update('\0');
93
+ }
94
+ }
95
+ };
96
+ await walk(root);
97
+ return hash.digest('hex');
98
+ }
99
+ async check(root) {
100
+ const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
101
+ requireThat(boundedText(pkg.name, 214) && /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(pkg.name) && boundedText(pkg.version, 64), 'invalid', 'Plugin needs an npm package name and version.');
102
+ requireThat(relativeFile(pkg.rhoPlugin), 'invalid', 'package.json must name its manifest in rhoPlugin.');
103
+ const manifestPath = await this.file(root, pkg.rhoPlugin);
104
+ requireThat((await fs.stat(manifestPath)).size <= 65536, 'limit', 'Plugin manifest exceeds 64 KiB.');
105
+ const m = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
106
+ requireThat(m.apiVersion === 1 && typeof m.id === 'string' && identifier.test(m.id) && boundedText(m.title, 120) && boundedText(m.description), 'invalid', 'Plugin manifest needs apiVersion 1, id, title and description.');
107
+ for (const [kind, maximum] of [['operations', 32], ['skills', 16], ['views', 16]]) {
108
+ requireThat(Array.isArray(m[kind]) && m[kind].length <= maximum && new Set(m[kind].map(x => x.id)).size === m[kind].length, 'invalid', `Invalid or duplicate ${kind}.`);
109
+ for (const entry of m[kind])
110
+ requireThat(typeof entry.id === 'string' && identifier.test(entry.id) && boundedText(entry.title, 120), 'invalid', `Invalid ${kind} entry.`);
111
+ }
112
+ if (m.operations.length) {
113
+ requireThat(m.host, 'invalid', 'Operations need a host entry.');
114
+ await this.file(root, m.host);
115
+ }
116
+ for (const op of m.operations) {
117
+ requireThat(boundedText(op.description) && ['read', 'native'].includes(op.effect) && Array.isArray(op.methods) && op.methods.length <= 16 && op.methods.every(x => typeof x === 'string' && /^[a-z][a-zA-Z.]+$/.test(x) && !x.startsWith('extension.') && !x.startsWith('session.') && !x.startsWith('project.')), 'invalid', 'Invalid operation effect or delegated Rho methods.');
118
+ requireThat(op.inputSchema?.type === 'object', 'invalid', 'An operation inputSchema must describe an object.');
119
+ ajv.compile(op.inputSchema);
120
+ }
121
+ for (const entry of [...m.skills, ...m.views]) {
122
+ const file = await this.file(root, entry.file);
123
+ requireThat((await fs.stat(file)).size <= this.limits.maxContentBytes, 'limit', 'Plugin entry exceeds extensions.maxContentBytes.');
124
+ }
125
+ return { name: pkg.name, version: pkg.version, manifest: m };
126
+ }
127
+ async verify(p) { requireThat(await this.hash(path.join(this.directory, 'versions', p.digest, 'node_modules')) === p.digest, 'conflict', 'Installed plugin bytes changed. Reinstall the package before use.'); }
128
+ async content(id, expected, kind, entryId) {
129
+ const p = this.selected(id, expected);
130
+ if (kind === 'view')
131
+ requireThat(p.enabled, 'unavailable', 'This plugin is disabled.');
132
+ await this.verify(p);
133
+ const entry = p.manifest[kind === 'skill' ? 'skills' : 'views'].find(x => x.id === entryId);
134
+ requireThat(entry, 'not_found', 'Plugin entry not found.');
135
+ return { pluginId: id, packageDigest: p.digest, entryId, kind, title: entry.title, text: await fs.readFile(await this.file(this.root(p), entry.file), 'utf8') };
136
+ }
137
+ install(source, replace, expected) {
138
+ return this.serial(async () => {
139
+ requireThat(!!replace === !!expected, 'invalid', 'An update needs both replace and expectedDigest.');
140
+ if (replace)
141
+ this.selected(replace, expected ?? '');
142
+ requireThat(replace || this.installed.length < this.limits.maxPlugins, 'limit', 'Installed plugins reached extensions.maxPlugins. Remove one before adding another.');
143
+ const versions = await fs.readdir(path.join(this.directory, 'versions'));
144
+ requireThat(versions.length < this.limits.maxVersions, 'limit', 'Retained package revisions reached extensions.maxVersions. Existing packages and receipts are kept; increase the configured limit before installing.');
145
+ const stage = await fs.mkdtemp(path.join(this.directory, 'staging-'));
146
+ try {
147
+ let spec = source, sourceCommit;
148
+ if (source.startsWith('git+https://')) {
149
+ const [remote, selector = 'HEAD'] = source.slice(4).split('#');
150
+ const [ref, subdir] = selector.split(':');
151
+ const url = new URL(remote);
152
+ requireThat(!url.username && !url.password && !url.search && /^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(ref) && (!subdir || relativeFile(subdir)), 'invalid', 'Use a public HTTPS Git source with #ref:relative/package. Credentials and query strings are not accepted.');
153
+ const sourceDirectory = path.join(stage, 'source');
154
+ await fs.mkdir(sourceDirectory);
155
+ const repo = await fs.realpath(sourceDirectory);
156
+ for (const args of [['init', '-q'], ['remote', 'add', 'origin', remote], ['fetch', '-q', '--depth=1', 'origin', ref], ['checkout', '-q', '--detach', 'FETCH_HEAD']])
157
+ await command('git', args, repo, this.limits.installTimeoutMs);
158
+ sourceCommit = (await command('git', ['rev-parse', 'HEAD'], repo, this.limits.installTimeoutMs)).trim();
159
+ spec = path.join(repo, subdir ?? '');
160
+ requireThat((await fs.realpath(spec)).startsWith(repo + path.sep) || spec === repo, 'scope', 'Git package escapes its source.');
161
+ }
162
+ else if (source.startsWith('https://')) {
163
+ const url = new URL(source);
164
+ requireThat(!url.username && !url.password && !url.search && url.pathname.endsWith('.tgz'), 'invalid', 'Use a public HTTPS .tgz without credentials or query strings.');
165
+ }
166
+ else {
167
+ const local = source.startsWith('file:') ? source.slice(5) : source;
168
+ const present = await fs.stat(path.resolve(local)).catch(() => null);
169
+ if (present) {
170
+ spec = path.resolve(local);
171
+ source = spec;
172
+ }
173
+ else
174
+ requireThat(/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-zA-Z0-9.*^~+_-]+)?$/.test(source), 'invalid', 'Use an npm package, existing local folder/tarball, HTTPS .tgz, or git+https source.');
175
+ }
176
+ const install = path.join(stage, 'install');
177
+ await fs.mkdir(install);
178
+ await atomicJson(path.join(install, 'package.json'), { name: 'rho-plugin-installation', version: '1.0.0', private: true });
179
+ const npmCli = /npm-cli\.js$/.test(process.env.npm_execpath ?? '') ? process.env.npm_execpath : process.platform === 'win32' ? path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js') : undefined;
180
+ await command(npmCli ? process.execPath : 'npm', [...(npmCli ? [npmCli] : []), 'install', '--ignore-scripts', '--engine-strict', '--omit=dev', '--no-audit', '--no-fund', '--install-links', '--', spec], install, this.limits.installTimeoutMs);
181
+ const pkg = JSON.parse(await fs.readFile(path.join(install, 'package.json'), 'utf8')), names = Object.keys(pkg.dependencies ?? {});
182
+ requireThat(names.length === 1, 'invalid', 'Install exactly one plugin package.');
183
+ const checked = await this.check(path.join(install, 'node_modules', names[0]));
184
+ requireThat(!replace || checked.manifest.id === replace, 'conflict', 'An update must keep the plugin identity.');
185
+ requireThat(replace || !this.installed.some(p => p.id === checked.manifest.id), 'conflict', 'Plugin is already installed. Use update with its current digest.');
186
+ await fs.rm(path.join(install, 'node_modules', '.package-lock.json'), { force: true });
187
+ const sha = await this.hash(path.join(install, 'node_modules'));
188
+ const selected = { id: checked.manifest.id, name: checked.name, version: checked.version, title: checked.manifest.title, description: checked.manifest.description, digest: sha, source, ...(sourceCommit ? { sourceCommit } : {}), enabled: replace ? this.describe(replace).enabled : true, installedAt: new Date().toISOString(), manifest: checked.manifest };
189
+ const destination = path.join(this.directory, 'versions', sha);
190
+ if (!versions.includes(sha))
191
+ await fs.rename(install, destination);
192
+ const next = [...this.installed.filter(p => p.id !== selected.id), selected];
193
+ await atomicJson(path.join(this.directory, 'installed.json'), next);
194
+ this.installed = next;
195
+ return selected;
196
+ }
197
+ finally {
198
+ await fs.rm(stage, { recursive: true, force: true });
199
+ }
200
+ });
201
+ }
202
+ manage(id, expected, action) {
203
+ return this.serial(async () => {
204
+ const p = this.selected(id, expected);
205
+ const next = this.installed.filter(x => x.id !== id);
206
+ if (action !== 'remove')
207
+ next.push({ ...p, enabled: action === 'enable' });
208
+ await atomicJson(path.join(this.directory, 'installed.json'), next);
209
+ this.installed = next;
210
+ return { pluginId: id, action, retained: 'Native sessions, project data, receipts and package revisions are retained.' };
211
+ });
212
+ }
213
+ async read(invocationId, caller, projectId) {
214
+ let receipt;
215
+ try {
216
+ receipt = JSON.parse(await fs.readFile(path.join(this.directory, 'receipts', invocationId + '.json'), 'utf8'));
217
+ }
218
+ catch (e) {
219
+ if (e.code !== 'ENOENT')
220
+ throw e;
221
+ requireThat(false, 'not_found', 'Plugin invocation not found.');
222
+ }
223
+ requireThat(receipt.caller === caller && receipt.projectId === projectId, 'scope', 'This plugin invocation belongs to another caller or project.');
224
+ return receipt;
225
+ }
226
+ async call(b, caller, projectRoot, dispatch) {
227
+ const invocationId = digest(canonical([caller, b.projectId, b.pluginId, b.changeId]));
228
+ const requestDigest = digest(canonical(b));
229
+ // Read the receipt before the selected package: an update or removal must not
230
+ // turn a retry of an acknowledged write into another invocation.
231
+ const earlier = await this.read(invocationId, caller, b.projectId).catch((e) => { if (e.kind !== 'not_found')
232
+ throw e; return null; });
233
+ if (earlier) {
234
+ requireThat(earlier.requestDigest === requestDigest, 'conflict', 'changeId already names a different plugin request.');
235
+ return earlier;
236
+ }
237
+ const active = this.running.get(invocationId);
238
+ if (active) {
239
+ const receipt = await active;
240
+ requireThat(receipt.requestDigest === requestDigest, 'conflict', 'changeId already names a different plugin request.');
241
+ return receipt;
242
+ }
243
+ const p = this.selected(b.pluginId, b.expectedDigest);
244
+ requireThat(p.enabled, 'unavailable', 'This plugin is disabled.');
245
+ const operation = p.manifest.operations.find(op => op.id === b.operation);
246
+ requireThat(operation, 'not_found', 'Plugin operation not found.');
247
+ const validate = ajv.compile(operation.inputSchema);
248
+ requireThat(validate(b.input), 'invalid', 'Input does not match the plugin operation schema.', { problems: validate.errors });
249
+ requireThat(this.running.size < this.limits.maxRunning, 'limit', 'Plugin calls reached extensions.maxRunning. Existing calls continue.');
250
+ const retained = operation.effect !== 'read';
251
+ if (retained) {
252
+ requireThat(this.receiptCount < this.limits.maxReceipts, 'limit', 'Plugin receipts reached extensions.maxReceipts. Existing receipts are kept; increase the limit before another write.');
253
+ this.receiptCount++;
254
+ }
255
+ const run = async () => {
256
+ await this.verify(p);
257
+ const receipt = { invocationId, caller, projectId: b.projectId, pluginId: p.id, packageDigest: p.digest, operation: b.operation, requestDigest, state: 'pending', startedAt: new Date().toISOString() };
258
+ const filename = path.join(this.directory, 'receipts', invocationId + '.json');
259
+ let writes = Promise.resolve();
260
+ const save = () => { const snapshot = structuredClone(receipt); const next = writes.then(() => atomicJson(filename, snapshot)); writes = next.catch(() => { }); return next; };
261
+ if (retained)
262
+ await save();
263
+ const delegate = async (method, body) => {
264
+ const entry = { method, request: body, state: 'pending' };
265
+ const entries = [...(receipt.delegations ?? []), entry];
266
+ requireThat(Buffer.byteLength(JSON.stringify({ ...receipt, delegations: entries })) <= this.limits.maxResultBytes, 'limit', 'Plugin receipt reached extensions.maxResultBytes before delegation. No further native request was submitted.');
267
+ receipt.delegations = entries;
268
+ if (retained)
269
+ await save();
270
+ try {
271
+ const value = await dispatch(method, body, operation.effect);
272
+ if (Buffer.byteLength(JSON.stringify({ ...receipt, candidate: value })) > this.limits.maxResultBytes)
273
+ throw new Error('Native reply exceeds extensions.maxResultBytes. Its original request is retained; observe output with the native owner.');
274
+ entry.value = value;
275
+ entry.state = 'completed';
276
+ if (retained)
277
+ await save();
278
+ return value;
279
+ }
280
+ catch (error) {
281
+ entry.state = 'unknown';
282
+ entry.error = error instanceof Error ? error.message : String(error);
283
+ if (retained)
284
+ await save();
285
+ throw error;
286
+ }
287
+ };
288
+ try {
289
+ const value = await this.execute(p, operation, b.input, invocationId, projectRoot, delegate);
290
+ requireThat(Buffer.byteLength(JSON.stringify({ ...receipt, value })) <= this.limits.maxResultBytes, 'limit', 'Plugin result exceeds extensions.maxResultBytes. Delegated native identities and available replies are retained.');
291
+ receipt.value = value;
292
+ receipt.state = 'completed';
293
+ }
294
+ catch (error) {
295
+ receipt.state = 'unknown';
296
+ receipt.error = error instanceof Error ? error.message : String(error);
297
+ }
298
+ receipt.endedAt = new Date().toISOString();
299
+ if (retained)
300
+ await save();
301
+ return receipt;
302
+ };
303
+ const promise = run();
304
+ this.running.set(invocationId, promise);
305
+ try {
306
+ return await promise;
307
+ }
308
+ finally {
309
+ this.running.delete(invocationId);
310
+ }
311
+ }
312
+ async execute(p, operation, input, invocationId, projectRoot, dispatch) {
313
+ const entry = await this.file(this.root(p), p.manifest.host);
314
+ return new Promise((resolve, reject) => {
315
+ const child = fork(new URL('./extension-worker.js', import.meta.url), [], { cwd: projectRoot, env: { PATH: process.env.PATH, HOME: process.env.HOME, SystemRoot: process.env.SystemRoot }, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], execArgv: [] });
316
+ this.workers.add(child);
317
+ let ended = false, calls = 0;
318
+ const finish = (error, value) => { if (ended)
319
+ return; ended = true; clearTimeout(timer); this.workers.delete(child); child.kill(); error ? reject(error) : resolve(value); };
320
+ const timer = setTimeout(() => finish(new Error('Plugin exceeded extensions.timeoutMs. Its native operations may still be running; observe their original identities.')), this.limits.timeoutMs);
321
+ child.on('error', error => finish(error));
322
+ child.on('exit', () => finish(new Error('Plugin exited before its result was received. The native outcome is unknown.')));
323
+ child.on('message', async (message) => {
324
+ if (ended)
325
+ return;
326
+ if (Buffer.byteLength(JSON.stringify(message)) > this.limits.maxResultBytes)
327
+ return finish(new Error('Plugin exceeded extensions.maxResultBytes. Native output remains with its owner; the plugin result is unknown.'));
328
+ if (message.type === 'result')
329
+ return finish(undefined, message.value);
330
+ if (message.type === 'error')
331
+ return finish(new Error(String(message.error)));
332
+ if (message.type === 'call') {
333
+ try {
334
+ requireThat(++calls <= this.limits.maxDelegations, 'limit', 'Plugin reached extensions.maxDelegations; no further Rho call was submitted.');
335
+ requireThat(operation.methods.includes(message.method), 'scope', 'Plugin did not declare this Rho method.');
336
+ const value = await dispatch(message.method, message.body);
337
+ if (!ended && child.connected)
338
+ child.send({ type: 'reply', id: message.id, value });
339
+ }
340
+ catch (error) {
341
+ if (!ended && child.connected)
342
+ child.send({ type: 'reply', id: message.id, error: error instanceof Error ? error.message : String(error) });
343
+ }
344
+ }
345
+ });
346
+ child.send({ type: 'start', entry, operation: operation.id, input, projectRoot, invocationId });
347
+ });
348
+ }
349
+ }
@@ -0,0 +1 @@
1
+ export declare function pluginCli(args: string[]): Promise<void>;