@rho-workspace/rho 0.5.4 → 0.5.5

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 (102) hide show
  1. package/dist/browser-access.js +3 -2
  2. package/dist/capabilities.d.ts +2 -2
  3. package/dist/capabilities.js +6 -1
  4. package/dist/cli.js +186 -173
  5. package/dist/contract.generated.d.ts +329 -4
  6. package/dist/contract.generated.js +1 -1
  7. package/dist/contracts/owners.generated.js +360 -6
  8. package/dist/discovery.d.ts +1 -0
  9. package/dist/discovery.js +3 -3
  10. package/dist/extension-contract.d.ts +99 -0
  11. package/dist/extension-contract.js +52 -0
  12. package/dist/extension-directory.d.ts +9 -0
  13. package/dist/extension-directory.js +11 -0
  14. package/dist/extension-worker.d.ts +1 -0
  15. package/dist/extension-worker.js +36 -0
  16. package/dist/extensions.d.ts +157 -0
  17. package/dist/extensions.js +349 -0
  18. package/dist/plugin-cli.d.ts +1 -0
  19. package/dist/plugin-cli.js +139 -0
  20. package/dist/plugin-inventory.json +18 -0
  21. package/dist/plugins.d.ts +3 -3
  22. package/dist/project-work.d.ts +3 -1
  23. package/dist/project-work.js +15 -2
  24. package/dist/protocol.d.ts +7 -0
  25. package/dist/protocol.js +4 -2
  26. package/dist/server.d.ts +4 -1
  27. package/dist/server.js +55 -8
  28. package/dist/web/.vite/manifest.json +61 -61
  29. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  30. package/dist/web/assets/choices-b0XhrGQV.js +1 -0
  31. package/dist/web/assets/{console-Dbdo-5oO.js → console-BSjLGe0T.js} +1 -1
  32. package/dist/web/assets/{dist-B2V0VDO0.js → dist-Buk0lwzI.js} +2 -2
  33. package/dist/web/assets/dist-Cw8hRIlT.js +1 -0
  34. package/dist/web/assets/dist-Tzoe_zm-.js +3 -0
  35. package/dist/web/assets/{dist-D25Vn17i.js → dist-foH9IKc6.js} +1 -1
  36. package/dist/web/assets/explore-in-BVUy3ZW_.js +1 -0
  37. package/dist/web/assets/graph-1OdIJ9l7.js +2 -0
  38. package/dist/web/assets/index-CsC3sTRq.css +1 -0
  39. package/dist/web/assets/{index-BHQIaDsx.js → index-CxmItXqa.js} +85 -85
  40. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-pVbbQUGP.js} +1 -1
  41. package/dist/web/assets/overview-B9UtVJ32.js +6 -0
  42. package/dist/web/assets/preview-CksD_5KS.js +1 -0
  43. package/dist/web/assets/{review-CmWou4Iv.js → review-CMxQlQ-C.js} +1 -1
  44. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-BFNSyrnD.js} +1 -1
  45. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-BGPSnF_I.js} +1 -1
  46. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-CsRoTHOh.js} +2 -2
  47. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-BbuuFWHV.js} +1 -1
  48. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-BvJlIMmA.js} +1 -1
  49. package/dist/web/assets/workspace-CWlkjLNx.js +5 -0
  50. package/dist/web/composition.json +67 -67
  51. package/dist/web/index.html +3 -3
  52. package/dist/web/renderer.json +1 -1
  53. package/dist/web-assets.d.ts +2 -1
  54. package/dist/web-assets.js +3 -2
  55. package/dist/work-contract.js +2 -2
  56. package/docs/HEADLESS.md +1 -1
  57. package/docs/PLUGIN-AUTHORING.md +4 -0
  58. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  59. package/node_modules/@rho-workspace/plugin-agent/README.md +39 -0
  60. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  61. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  62. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  63. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  64. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +15 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +73 -0
  67. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  71. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  76. package/node_modules/@rho-workspace/plugin-agent/package.json +5 -1
  77. package/package.json +5 -3
  78. package/protocol/index.json +58 -4
  79. package/protocol/operations/agent.sessions.list.json +100 -0
  80. package/protocol/operations/agent.sessions.read.json +266 -0
  81. package/protocol/operations/extension.call.json +132 -0
  82. package/protocol/operations/extension.content.json +78 -0
  83. package/protocol/operations/extension.describe.json +195 -0
  84. package/protocol/operations/extension.install.json +198 -0
  85. package/protocol/operations/extension.list.json +123 -0
  86. package/protocol/operations/extension.manage.json +55 -0
  87. package/protocol/operations/extension.read.json +112 -0
  88. package/protocol/operations/project.work.read.json +3 -2
  89. package/protocol/operations/session.grant.json +9 -0
  90. package/scripts/third-party-acceptance.mjs +77 -0
  91. package/vendor/composition.json +85 -85
  92. package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
  93. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  94. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  95. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  96. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  97. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  98. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  99. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  100. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  101. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  102. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
@@ -0,0 +1,190 @@
1
+ # Develop and install third-party plugins
2
+
3
+ Rho can install independent npm packages while it is running. A package supplies
4
+ operations, Skills and standalone HTML views; it does not require rebuilding Rho
5
+ or becoming a base dependency. The first consumer is the optional
6
+ [sclet adapter](https://github.com/YuLab-SMU/Rho-plugins/tree/main/examples/sclet-plugin).
7
+ Its scientific implementation remains in sclet and its objects remain in the
8
+ project's shared R session. See the [observed episode](THIRD-PARTY-PLUGINS-EPISODE.md).
9
+
10
+ This is source functionality after Rho 0.5.4, not a new npm release. Build the
11
+ current checkout with `npm run build` and use `node dist/cli.js` in place of
12
+ `rho` until a distribution containing it is installed.
13
+
14
+ ## Install and manage
15
+
16
+ Open a project, then **Plugins → Third-party plugins**. Install a directory entry
17
+ or enter a package source. The installed card shows its version, source, enabled
18
+ state and content digest. Open its view, read its guide, update, disable or remove
19
+ it there. An invalid source or failed update leaves the previous selection intact.
20
+
21
+ Only install packages you trust. Hosts run native code with the Rho process user's
22
+ OS permissions. Disabling npm lifecycle scripts prevents install hooks; it does
23
+ not make plugin execution a sandbox. No approval is added to already authorized
24
+ analysis. Paired browsers and Agent connections cannot install or manage packages.
25
+
26
+ The equivalent commands use the running service's private connection file:
27
+
28
+ ```sh
29
+ rho plugin add /absolute/path/my-plugin
30
+ rho plugin add /absolute/path/my-plugin-0.1.0.tgz
31
+ rho plugin add @your-org/rho-plugin@0.1.0
32
+ rho plugin add 'git+https://github.com/your-org/repository.git#COMMIT:packages/plugin'
33
+ rho plugin list
34
+ rho plugin update my-plugin
35
+ rho plugin update my-plugin --source @your-org/rho-plugin@0.2.0
36
+ rho plugin disable my-plugin
37
+ rho plugin enable my-plugin
38
+ rho plugin remove my-plugin
39
+ ```
40
+
41
+ Add `--connection /absolute/service-state/connection.json` for a nondefault
42
+ service. The default is `~/Rho/state/connection.json`. These commands do not start
43
+ a service. Never share the connection file. Public HTTPS `.tgz` URLs are also
44
+ accepted. Git sources require Git; package staging requires npm. Git submodules
45
+ and build hooks are not run. Build and include runtime files before distribution.
46
+
47
+ Installation stages an isolated dependency tree using npm with `--ignore-scripts`
48
+ and `--install-links`, validates the manifest and contained files, hashes the tree,
49
+ then atomically selects it. The source selector, resolved Git commit when present,
50
+ package version and runtime-tree digest remain visible. Listing does not import
51
+ plugin code. Content and calls verify the installed tree before use.
52
+
53
+ Disable blocks new calls and views. Remove takes the package out of the installed
54
+ selection. Both preserve native tasks, R sessions, project files and receipts.
55
+ Package revisions are retained in the service's `plugins/versions` directory;
56
+ remove is not a disk-purge command. Closing a view only releases its renderer.
57
+
58
+ ## Create a package
59
+
60
+ ```sh
61
+ rho plugin create my-plugin --id lab.my-plugin
62
+ rho plugin check ./my-plugin
63
+ rho plugin dev ./my-plugin
64
+ ```
65
+
66
+ `create` refuses to overwrite a directory. The generated package has a working
67
+ greeting operation, Skill and view. `dev` installs checked snapshots on source
68
+ changes, ignoring `.git` and `node_modules`. Reopen a view to load a new snapshot.
69
+ Invalid edits retain the last working installation. No Rho rebuild is needed.
70
+
71
+ | File | Responsibility |
72
+ | --- | --- |
73
+ | `package.json` | npm name/version, runtime files and `rhoPlugin: "rho-plugin.json"` |
74
+ | `rho-plugin.json` | API version 1, unique plugin id, title, description and entries |
75
+ | `host.mjs` | `run(operation, input, context)` over the domain's public capabilities |
76
+ | `SKILL.md` | When/how to use the package, prerequisites, failures and result limits |
77
+ | `view.html` | Prebuilt HTML with inline scripts/styles; may bundle a framework |
78
+
79
+ Each operation declares `id`, `title`, `description`, `effect` (`read` or
80
+ `native`), the Rho `methods` it delegates, and an object `inputSchema`. Each Skill
81
+ or view declares `id`, `title` and a package-relative `file`. The manifest's
82
+ `operations`, `skills` and `views` arrays are required, but may be empty. A host
83
+ is required only when operations exist. The checker validates entry containment,
84
+ API compatibility, identifiers, duplicate entries, schemas and entry sizes.
85
+
86
+ ```js
87
+ export async function run(operation, input, context) {
88
+ if (operation === 'read-sessions') {
89
+ const value = await context.call('kernel.read', { limit: 1 });
90
+ return { sessions: value.sessions };
91
+ }
92
+ throw new Error('Unknown operation');
93
+ }
94
+ ```
95
+
96
+ `context.projectRoot` is the selected project, `context.invocationId` identifies
97
+ this call, and `context.changeId(label)` supplies a stable delegated write id.
98
+ `context.call(method, body)` validates the ordinary Rho schema, original caller's
99
+ grant, current project and owner availability. Declare every delegated method.
100
+ A read operation cannot delegate a mutating method. It must also keep its own
101
+ native work read-only; native host code is trusted, not OS-confined.
102
+
103
+ The host runs in a disposable child process for each call, without the service
104
+ token. Keep long work in its actual native owner and return its handle promptly.
105
+ For example sclet submits `kernel.run` with `waitMs: 0`, then observes the same
106
+ cell with `kernel.read`. Do not create a second scientific state store in Rho.
107
+
108
+ ## Agent and view entries
109
+
110
+ Agents discover installed packages through `extension.list`, read their schema
111
+ with `extension.describe`, and read a Skill through `extension.content` with
112
+ `kind: "skill"`. These operations are available through the existing layered MCP
113
+ discovery. They then call:
114
+
115
+ ```json
116
+ {
117
+ "pluginId": "lab.my-plugin",
118
+ "expectedDigest": "64-character digest from extension.describe",
119
+ "operation": "greet",
120
+ "input": { "name": "World" },
121
+ "changeId": "original-request-1"
122
+ }
123
+ ```
124
+
125
+ This is the body of `extension.call`; direct HTTP clients also supply `projectId`.
126
+ The returned receipt separates plugin completion from the native result in
127
+ `value`. Native invocations retain pending/completed/unknown receipts and delegated
128
+ requests/replies. Repeat the **exact** request to observe the same receipt, even
129
+ after update or removal. `extension.read` reads it by `invocationId` for the same
130
+ caller/project. A restart converts pending receipts to unknown; it never replays
131
+ them. Read-only calls do not accumulate durable receipts. No plugin receipt makes
132
+ native work Core-managed or establishes scientific validity.
133
+
134
+ A view runs in an iframe with an opaque origin, no service credential or storage
135
+ access, and no network loading. Bundle assets inline or as data URLs. Its bridge
136
+ is restricted to the selected package and digest:
137
+
138
+ ```js
139
+ const receipt = await rho.call('greet', { name: 'World' });
140
+ output.textContent = receipt.state === 'completed'
141
+ ? receipt.value.greeting : receipt.error;
142
+ ```
143
+
144
+ Pass a third argument to `rho.call` to retain an explicit original changeId.
145
+ The default generates one per deliberate call. A stale digest is refused; reopen
146
+ the view after an update. Several views may be declared and selected in the plugin
147
+ panel. These runtime views currently live on Plugins, not in Studio's resource
148
+ binding/layout contract. Existing compiled React extensions still use
149
+ [UI extension authoring](PLUGIN-AUTHORING.md).
150
+
151
+ ## Contribute and distribute
152
+
153
+ Keep your repository, license, dependencies and release history independent. Check
154
+ the packed artifact, not just source: `npm pack --ignore-scripts`, then install
155
+ that tarball in a disposable Rho project. Exercise missing prerequisites, an input
156
+ error, recovery, update and removal. Record the Rho/API version, package digest,
157
+ native versions and actual result. Preserve user data and avoid credentials in
158
+ examples or diagnostics. Publish to your own npm namespace or share a public Git
159
+ source/tarball; Rho does not publish packages on your behalf.
160
+
161
+ No central registration is needed to install a package. To be discoverable in the
162
+ bundled directory, submit a PR editing
163
+ [`src/extension-directory.ts`](../src/extension-directory.ts), linking the public
164
+ source, license, author guide and a reproducible acceptance record. Propose an
165
+ immutable Git commit or exact npm version. Maintainers check the declared scope;
166
+ directory inclusion is not a security audit, release signature or scientific
167
+ endorsement. Future releases of the package can update that entry independently.
168
+
169
+ ## Resource limits and current scope
170
+
171
+ Use the service's `--limits` JSON file with an `extensions` object to override
172
+ limits. Exceeding a limit refuses new work, retains the previous selected package
173
+ and obtained receipts, or records an unknown result after an interrupted call.
174
+
175
+ | Protected resource | Default configuration |
176
+ | --- | --- |
177
+ | Installed selections / retained revisions | `maxPlugins: 32`, `maxVersions: 128` |
178
+ | Admitted runtime tree | `maxFiles: 20000`, `maxPackageBytes: 134217728` |
179
+ | Skill/view bytes | `maxContentBytes: 524288` |
180
+ | Durable invocations / receipt and result bytes | `maxReceipts: 2000`, `maxResultBytes: 1048576` |
181
+ | Concurrent calls / delegated calls per invocation | `maxRunning: 4`, `maxDelegations: 32` |
182
+ | Worker / each install subprocess | `timeoutMs: 30000`, `installTimeoutMs: 180000` |
183
+
184
+ The tree-size check applies after npm staging; it is not a disk quota on npm/Git.
185
+ A timed-out worker does not prove a delegated native task stopped. Observe its
186
+ original identity. Retention limits refuse rather than silently evict identities;
187
+ raise them deliberately when appropriate. Manifests are capped at 64 KiB, with
188
+ 32 operations and 16 entries of each content kind. The browser permits four
189
+ pending calls per view. macOS with Node 22 is observed in this episode; Windows,
190
+ Linux and independent developer usability have not been established for this path.
@@ -390,3 +390,42 @@ fetched; an explicit response excludes replies already present in that context.
390
390
  ACP retains Codex's native MCP spelling while identifying its operation, and names
391
391
  an otherwise unnamed permission from the original tool update. These observations
392
392
  do not expand native permission or grant access to arbitrary session history.
393
+
394
+ ## External conversations
395
+
396
+ Agent 0.4.29 exports `dsh-session` for a Harness adapter to retain a read-only
397
+ observation of a native version-4 session. `observeDshSession(projectRoot, header,
398
+ events)` requires the real native working folder to equal the selected project.
399
+ It preserves session, turn and tool-call identities. Repeated cuts replace the
400
+ same record; a changed prefix or backwards cursor is refused. It never creates
401
+ an Agent, executes a tool, changes a grant or resumes a runtime.
402
+
403
+ `external-session` supplies bounded `listExternalSessions` and
404
+ `readExternalSession` reads. The operation contracts are `agent.sessions.list`
405
+ and `agent.sessions.read`; an embedding application supplies authorization.
406
+ `ui-external-session` renders prompts, visible replies, operations and results,
407
+ with input explicitly retained by the native client. Reasoning, injected context
408
+ and system messages are excluded; credential-shaped strings are redacted.
409
+ This is an observation of the client's reports, not an independent execution proof.
410
+
411
+ A successful resource-registration reply carries its exact resource identity and
412
+ version. The application must verify that tuple against its own project resources.
413
+ A native deliverable without a receipt is only a reported path, with no historical
414
+ bytes asserted. Tool return, native exit code and turn completion remain separate.
415
+ An unfinished turn with no observation for 10 seconds has an unknown outcome.
416
+
417
+ `externalSessionLimits` names the fixed local observation budgets: 64 sessions
418
+ per project, 200 retained turns per session, 256 tools per turn, 32,768 source
419
+ events / 32 MiB per cut, 32,768 characters per prompt/reply and earlier-reply
420
+ collection, 8,192 characters per tool field, and 4 MiB per stored record.
421
+ Text/tools/earlier turns beyond their budgets are omitted with counts. An
422
+ oversized source, conflicting cut, invalid record or busy writer retains the
423
+ previous record and reports failure. Storage is `.rho/agent/external`, contained
424
+ in the project without symlink directories; atomic replacement leaves readers a
425
+ complete record. A positively dead PID lock can be reclaimed; an unknown/live
426
+ lock cannot. Reads create no directories or runtime state. These limits protect
427
+ local storage and observation work; they do not limit the native Agent.
428
+
429
+ `test/external-session.test.mjs` checks identity, prefix conflicts, omissions,
430
+ missing outcomes, redaction, project containment, retained failure and pagination.
431
+ The Rho application records its separate real Harness/browser acceptance.
@@ -10,5 +10,6 @@ export interface CaptureOptions {
10
10
  maxEventBytes?: number;
11
11
  }
12
12
  export declare function canonicalActivity(value: any): string;
13
+ export declare function activityPayload(value: any, options: CaptureOptions): any;
13
14
  export declare function normalizeClaude(records: any[], sessionId: string, project: string, options?: CaptureOptions): ObservedEvent[];
14
15
  export declare function normalizeDsh(records: any[], options?: CaptureOptions): ObservedEvent[];
@@ -9,7 +9,7 @@ export function canonicalActivity(value) {
9
9
  const sensitive = /(?:^|[_-])(?:authorization|cookie|password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|token)$/i;
10
10
  const hidden = /^(?:thinking|reasoning|reasoning_content|signature)$/i;
11
11
  const txt = (v) => typeof v === 'string' ? v : Array.isArray(v) ? v.filter(b => b?.type === 'text').map(b => b.text).join('\n') : '';
12
- function payload(value, options) {
12
+ export function activityPayload(value, options) {
13
13
  let redacted = false, truncated = false;
14
14
  const cap = options.maxFieldBytes ?? 8192;
15
15
  const identities = new Set(['native_id', 'call_id', 'root_call_id', 'job_id', 'parent_message_id']);
@@ -76,7 +76,7 @@ function inputSummary(input) {
76
76
  function chained(items, options) {
77
77
  let prefix = hash(activityNormalizer);
78
78
  return items.map(({ kind, data }, sequence) => {
79
- const safe = payload(data, options);
79
+ const safe = activityPayload(data, options);
80
80
  prefix = hash(prefix + canonicalActivity({ kind, data: safe }));
81
81
  return { sequence, kind, data: { ...safe, prefix_sha256: prefix } };
82
82
  });
@@ -0,0 +1,6 @@
1
+ import { type ExternalSessionRecord } from './external-session.js';
2
+ export declare function externalProjectPath(root: string, input: unknown): string | null;
3
+ /** Project the native committed log. System prompts, injected context and reasoning
4
+ * are excluded. Original session, turn and call identities survive every backfill. */
5
+ export declare function projectDshSession(root: string, header: any, events: readonly any[], observedAt?: string): ExternalSessionRecord;
6
+ export declare function observeDshSession(root: string, header: any, events: readonly any[]): import("./external-session.js").ExternalSessionSummary;
@@ -0,0 +1,174 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { activityPayload, canonicalActivity } from './activity-normalize.js';
5
+ import { externalSessionId, retainedExternalSession, saveExternalSession } from './external-session.js';
6
+ import { externalSessionLimits as limits } from './external-session-contract.js';
7
+ const text = (v) => typeof v === 'string' ? v : Array.isArray(v) ? v.filter(b => b?.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n') : '';
8
+ const time = (v) => { const n = typeof v === 'number' ? v : Date.parse(v); if (!Number.isFinite(n))
9
+ throw new Error('Native event has no valid timestamp.'); return new Date(n).toISOString(); };
10
+ const hash = (v) => createHash('sha256').update(canonicalActivity(v)).digest('hex');
11
+ const safe = (value, cap = limits.maxTextChars) => (activityPayload({ text: value.replace(/\b([\w-]*(?:token|cookie|authorization)["']?\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s'";]+)/gi, '$1[redacted]') }, { content: true, maxFieldBytes: cap * 4, maxEventBytes: cap * 8 }).text ?? '').slice(0, cap).replace(/[\uD800-\uDBFF]$/, '');
12
+ export function externalProjectPath(root, input) {
13
+ if (typeof input !== 'string' || input.length > 1024 || input.includes('\0'))
14
+ return null;
15
+ const relative = path.relative(root, path.resolve(root, input)).split(path.sep).join('/');
16
+ return relative && !relative.startsWith('../') && !path.isAbsolute(relative) && !relative.split('/').some(p => ['..', '.rho', '.git'].includes(p)) ? relative : null;
17
+ }
18
+ function paths(root, args, content) {
19
+ const candidates = [args?.path, args?.file_path, args?.filename, args?.declaration?.path];
20
+ // These are explicitly labelled mentions, never inferred file writes or historical snapshots.
21
+ for (const m of content.matchAll(/(?<![\w/.:\-])\/?(?:[\w.\-\u0080-\uFFFF]+\/)*[\w.\-\u0080-\uFFFF]+\.(?:R|r|py|md|csv|tsv|png|svg|pdf|json|txt|qmd|Rmd)\b/g))
22
+ candidates.push(m[0]);
23
+ return [...new Set(candidates.map(p => externalProjectPath(root, p)).filter((p) => !!p))].slice(0, 32);
24
+ }
25
+ /** Project the native committed log. System prompts, injected context and reasoning
26
+ * are excluded. Original session, turn and call identities survive every backfill. */
27
+ export function projectDshSession(root, header, events, observedAt = new Date().toISOString()) {
28
+ root = fs.realpathSync(root);
29
+ if (!header || header.version !== 4 || typeof header.id !== 'string' || !/^[A-Za-z0-9._:@-]{1,128}$/.test(header.id) || typeof header.cwd !== 'string' || fs.realpathSync(header.cwd) !== root)
30
+ throw new Error('Harness session does not match the selected project.');
31
+ if (events.length > limits.maxEvents || Buffer.byteLength(JSON.stringify(events)) > limits.maxSourceBytes)
32
+ throw new Error('Harness observation exceeds source limits; previous observation retained.');
33
+ const turns = [], byTurn = new Map(), tools = new Map();
34
+ let current, title = '', lastAt = time(header.createdAt), omittedTurns = 0;
35
+ const makeTurn = (id, at) => {
36
+ let t = byTurn.get(id);
37
+ if (t)
38
+ return t;
39
+ t = { id: 'turn:' + id, at, state: 'unknown', prompt: '', reply: '', messages: [], tools: [], results: [], omittedTools: 0, omittedChars: 0 };
40
+ byTurn.set(id, t);
41
+ turns.push(t);
42
+ if (turns.length > limits.maxTurns) {
43
+ const gone = turns.shift();
44
+ omittedTurns++;
45
+ for (const [k, v] of tools)
46
+ if (v.turn === gone)
47
+ tools.delete(k);
48
+ for (const [k, v] of byTurn)
49
+ if (v === gone)
50
+ byTurn.delete(k);
51
+ }
52
+ return t;
53
+ };
54
+ const appendText = (t, field, value, replace = false) => {
55
+ const full = replace ? value : t[field] + (t[field] ? '\n\n' : '') + value;
56
+ t[field] = safe(full);
57
+ t.omittedChars += Math.max(0, full.length - t[field].length);
58
+ };
59
+ for (let i = 0; i < events.length; i++) {
60
+ const e = events[i];
61
+ if (e.seq !== i)
62
+ throw new Error('Harness source has a missing or reordered native event.');
63
+ const d = e.data ?? {}, at = time(e.time);
64
+ lastAt = at;
65
+ if (e.type === 'session/title' && typeof d.title === 'string')
66
+ title = safe(d.title, 512);
67
+ if (e.type === 'turn/start') {
68
+ current = makeTurn(d.turn, at);
69
+ current.state = 'running';
70
+ }
71
+ if (e.type === 'user/message' && d.source?.kind === 'user') {
72
+ if (!current)
73
+ current = makeTurn(e.seq, at);
74
+ appendText(current, 'prompt', text(d.content));
75
+ }
76
+ if (e.type === 'assistant/message') {
77
+ const t = byTurn.get(d.turn) ?? current;
78
+ if (!t)
79
+ continue;
80
+ const message = text(d.message?.content);
81
+ if (message) {
82
+ if (t.reply)
83
+ t.messages.push({ at, text: t.reply });
84
+ while (t.messages.length > 200 || t.messages.reduce((n, m) => n + m.text.length, 0) > limits.maxTextChars)
85
+ t.omittedChars += t.messages.shift().text.length;
86
+ appendText(t, 'reply', message, true);
87
+ }
88
+ }
89
+ if (e.type === 'tool/call') {
90
+ const t = byTurn.get(d.turn) ?? current;
91
+ if (!t || typeof d.callId !== 'string' || typeof d.name !== 'string')
92
+ continue;
93
+ if (tools.has(d.callId))
94
+ throw new Error('Harness call identity was reused.');
95
+ if (t.tools.length >= limits.maxTools) {
96
+ t.omittedTools++;
97
+ continue;
98
+ }
99
+ let args = d.arguments;
100
+ if (typeof args === 'string') {
101
+ try {
102
+ args = JSON.parse(args);
103
+ }
104
+ catch { }
105
+ }
106
+ const name = d.name === 'mcp__rho__rho_call' && typeof args?.operation === 'string' ? args.operation : d.name;
107
+ const input = args?.input ?? args;
108
+ const detail = safe(typeof input?.command === 'string' ? input.command : JSON.stringify(input) ?? '', limits.maxDetailChars);
109
+ const tool = { id: d.callId, at, name, detail, output: '', outcome: 'requested', paths: paths(root, input, detail) };
110
+ t.tools.push(tool);
111
+ tools.set(d.callId, { turn: t, tool });
112
+ }
113
+ if (e.type === 'tool/result') {
114
+ const message = d.message, item = tools.get(message?.toolCallId ?? message?.source?.callId);
115
+ if (!item)
116
+ continue;
117
+ const output = text(message?.content);
118
+ item.tool.output = safe(output, limits.maxDetailChars);
119
+ item.tool.outcome = message?.isError ? 'failed' : 'returned';
120
+ item.turn.omittedChars += Math.max(0, output.length - item.tool.output.length);
121
+ // Only a successful Rho registration/capture reply asserts an exact resource version.
122
+ if (['resource.register', 'resource.capture', 'agent.activity.capture'].includes(item.tool.name) && !message?.isError) {
123
+ try {
124
+ const response = JSON.parse(output), r = response.value?.resource ?? response.value;
125
+ const relative = externalProjectPath(root, r?.path);
126
+ if (response.ok === true && relative && /^[a-f0-9]{64}$/.test(r.id) && /^[a-f0-9]{64}$/.test(r.version) && typeof r.projectId === 'string' && item.turn.results.length < 64)
127
+ item.turn.results.push({ path: relative, id: r.id, version: r.version, projectId: r.projectId, toolId: item.tool.id });
128
+ }
129
+ catch { /* The bounded receipt can be incomplete; never fabricate a resource. */ }
130
+ }
131
+ }
132
+ if (e.type === 'job/settled') {
133
+ const item = tools.get(d.toolCall?.rootCallId ?? d.toolCall?.callId), exit = typeof d.detail === 'string' ? /^exit code: (-?\d+)$/.exec(d.detail) : null;
134
+ if (item && exit)
135
+ item.tool.exitCode = Number(exit[1]);
136
+ }
137
+ if (e.type === 'deliverables/presented') {
138
+ const t = byTurn.get(d.turn) ?? current;
139
+ if (!t)
140
+ continue;
141
+ for (const file of d.files ?? []) {
142
+ const relative = externalProjectPath(root, file.path);
143
+ if (relative && !t.results.some(r => r.path === relative) && t.results.length < 64)
144
+ t.results.push({ path: relative, toolId: d.callId ?? 'event:' + e.seq });
145
+ }
146
+ }
147
+ if (e.type === 'turn/end') {
148
+ const t = byTurn.get(d.turn);
149
+ if (!t)
150
+ continue;
151
+ t.state = d.reason?.kind === 'completed' ? 'completed' : d.reason?.kind === 'cancelled' ? 'stopped' : d.reason?.kind === 'error' ? 'failed' : 'unknown';
152
+ t.endedAt = at;
153
+ for (const tool of t.tools)
154
+ if (tool.outcome === 'requested')
155
+ tool.outcome = 'unknown';
156
+ }
157
+ }
158
+ const summary = { id: externalSessionId('dsh', header.id), producer: 'dsh', sessionId: header.id, title: title || turns.find(t => t.prompt)?.prompt.slice(0, 160) || 'Harness conversation', lastAt, observedAt,
159
+ state: turns.at(-1)?.state ?? 'unknown', turns: turns.length + omittedTurns, sourceCursor: events.length, omittedTurns, inputOwner: 'external' };
160
+ const result = { format: 1, projectRoot: root, summary, turns, sourceDigest: hash(events) };
161
+ // A suffix is retained at the record budget; gaps are explicit, earlier files stay untouched.
162
+ while (Buffer.byteLength(JSON.stringify(result)) > limits.maxRecordBytes && result.turns.length > 1) {
163
+ result.turns.shift();
164
+ result.summary.omittedTurns++;
165
+ }
166
+ if (Buffer.byteLength(JSON.stringify(result)) > limits.maxRecordBytes)
167
+ throw new Error('Harness turn exceeds maxRecordBytes; previous observation retained.');
168
+ return result;
169
+ }
170
+ export function observeDshSession(root, header, events) {
171
+ const old = retainedExternalSession(root, 'dsh', header.id);
172
+ const value = projectDshSession(root, header, events);
173
+ return saveExternalSession(root, value, old ? hash(events.slice(0, old.summary.sourceCursor)) : undefined);
174
+ }
@@ -0,0 +1,82 @@
1
+ export declare const externalSessionLimits: {
2
+ maxSessions: number;
3
+ maxTurns: number;
4
+ maxTools: number;
5
+ maxRecordBytes: number;
6
+ maxEvents: number;
7
+ maxSourceBytes: number;
8
+ maxTextChars: number;
9
+ maxDetailChars: number;
10
+ };
11
+ export declare const externalResultSchema: {
12
+ type: string;
13
+ properties: Record<string, any>;
14
+ required: string[];
15
+ additionalProperties: boolean;
16
+ };
17
+ export declare const externalToolSchema: {
18
+ type: string;
19
+ properties: Record<string, any>;
20
+ required: string[];
21
+ additionalProperties: boolean;
22
+ };
23
+ export declare const externalTurnSchema: {
24
+ type: string;
25
+ properties: Record<string, any>;
26
+ required: string[];
27
+ additionalProperties: boolean;
28
+ };
29
+ export declare const externalSessionSummarySchema: {
30
+ type: string;
31
+ properties: Record<string, any>;
32
+ required: string[];
33
+ additionalProperties: boolean;
34
+ };
35
+ export declare const externalSessionSchemas: {
36
+ summary: {
37
+ type: string;
38
+ properties: Record<string, any>;
39
+ required: string[];
40
+ additionalProperties: boolean;
41
+ };
42
+ turn: {
43
+ type: string;
44
+ properties: Record<string, any>;
45
+ required: string[];
46
+ additionalProperties: boolean;
47
+ };
48
+ };
49
+ export declare const externalSessionOperations: {
50
+ 'agent.sessions.list': {
51
+ inputSchema: {
52
+ type: string;
53
+ properties: Record<string, any>;
54
+ required: string[];
55
+ additionalProperties: boolean;
56
+ };
57
+ outputSchema: {
58
+ type: string;
59
+ properties: Record<string, any>;
60
+ required: string[];
61
+ additionalProperties: boolean;
62
+ };
63
+ requires: never[];
64
+ effect: "read";
65
+ };
66
+ 'agent.sessions.read': {
67
+ inputSchema: {
68
+ type: string;
69
+ properties: Record<string, any>;
70
+ required: string[];
71
+ additionalProperties: boolean;
72
+ };
73
+ outputSchema: {
74
+ type: string;
75
+ properties: Record<string, any>;
76
+ required: string[];
77
+ additionalProperties: boolean;
78
+ };
79
+ requires: never[];
80
+ effect: "read";
81
+ };
82
+ };
@@ -0,0 +1,15 @@
1
+ // A retained observation of another client's conversation. It has no input operation.
2
+ const text = { type: 'string' }, count = { type: 'integer', minimum: 0 };
3
+ const obj = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
4
+ const array = (items, maxItems) => ({ type: 'array', items, maxItems });
5
+ export const externalSessionLimits = { maxSessions: 64, maxTurns: 200, maxTools: 256, maxRecordBytes: 4194304, maxEvents: 32768, maxSourceBytes: 33554432, maxTextChars: 32768, maxDetailChars: 8192 };
6
+ const state = { enum: ['running', 'completed', 'failed', 'stopped', 'unknown'] };
7
+ export const externalResultSchema = obj({ path: text, id: text, version: text, projectId: text, toolId: text }, ['path', 'toolId']);
8
+ export const externalToolSchema = obj({ id: text, at: text, name: text, detail: text, output: text, outcome: { enum: ['requested', 'returned', 'failed', 'unknown'] }, exitCode: { type: 'integer' }, paths: array(text, 32) }, ['id', 'at', 'name', 'detail', 'output', 'outcome', 'paths']);
9
+ export const externalTurnSchema = obj({ id: text, at: text, endedAt: text, state, prompt: text, reply: text, messages: array(obj({ at: text, text }), 200), tools: array(externalToolSchema, externalSessionLimits.maxTools), results: array(externalResultSchema, 64), omittedTools: count, omittedChars: count }, ['id', 'at', 'state', 'prompt', 'reply', 'messages', 'tools', 'results', 'omittedTools', 'omittedChars']);
10
+ export const externalSessionSummarySchema = obj({ id: { type: 'string', pattern: '^x-[a-f0-9]{64}$' }, producer: text, sessionId: text, title: text, lastAt: text, observedAt: text, state, turns: count, sourceCursor: count, omittedTurns: count, problem: text, inputOwner: { const: 'external' } }, ['id', 'producer', 'sessionId', 'title', 'lastAt', 'observedAt', 'state', 'turns', 'sourceCursor', 'omittedTurns', 'inputOwner']);
11
+ export const externalSessionSchemas = { summary: externalSessionSummarySchema, turn: externalTurnSchema };
12
+ export const externalSessionOperations = {
13
+ 'agent.sessions.list': { inputSchema: obj({}), outputSchema: obj({ sessions: array(externalSessionSummarySchema, 64), omitted: count }), requires: [], effect: 'read' },
14
+ 'agent.sessions.read': { inputSchema: obj({ session: { type: 'string', pattern: '^x-[a-f0-9]{64}$' }, before: count, limit: { type: 'integer', minimum: 1, maximum: 20 } }, ['session']), outputSchema: obj({ session: externalSessionSummarySchema, first: count, turns: array(externalTurnSchema, 20) }), requires: [], effect: 'read' },
15
+ };
@@ -0,0 +1,73 @@
1
+ export { externalSessionLimits } from './external-session-contract.js';
2
+ export interface ExternalTool {
3
+ id: string;
4
+ at: string;
5
+ name: string;
6
+ detail: string;
7
+ output: string;
8
+ outcome: 'requested' | 'returned' | 'failed' | 'unknown';
9
+ exitCode?: number;
10
+ paths: string[];
11
+ }
12
+ export interface ExternalResult {
13
+ path: string;
14
+ toolId: string;
15
+ id?: string;
16
+ version?: string;
17
+ projectId?: string;
18
+ }
19
+ export interface ExternalTurn {
20
+ id: string;
21
+ at: string;
22
+ endedAt?: string;
23
+ state: 'running' | 'completed' | 'failed' | 'stopped' | 'unknown';
24
+ prompt: string;
25
+ reply: string;
26
+ messages: Array<{
27
+ at: string;
28
+ text: string;
29
+ }>;
30
+ tools: ExternalTool[];
31
+ results: ExternalResult[];
32
+ omittedTools: number;
33
+ omittedChars: number;
34
+ }
35
+ export interface ExternalSessionSummary {
36
+ id: string;
37
+ producer: string;
38
+ sessionId: string;
39
+ title: string;
40
+ lastAt: string;
41
+ observedAt: string;
42
+ state: ExternalTurn['state'];
43
+ turns: number;
44
+ sourceCursor: number;
45
+ omittedTurns: number;
46
+ problem?: string;
47
+ inputOwner: 'external';
48
+ }
49
+ export interface ExternalSessionRecord {
50
+ format: 1;
51
+ projectRoot: string;
52
+ summary: ExternalSessionSummary;
53
+ turns: ExternalTurn[];
54
+ sourceDigest: string;
55
+ }
56
+ export declare const externalSessionId: (producer: string, session: string) => string;
57
+ export declare function listExternalSessions(root: string): {
58
+ sessions: ExternalSessionSummary[];
59
+ omitted: number;
60
+ };
61
+ export declare function readExternalSession(root: string, input: {
62
+ session: string;
63
+ before?: number;
64
+ limit?: number;
65
+ }): {
66
+ session: ExternalSessionSummary;
67
+ first: number;
68
+ turns: ExternalTurn[];
69
+ };
70
+ /** Native adapters retain an observation under their existing filesystem authority.
71
+ * No managed execution, Agent input, grant change or runtime startup is involved. */
72
+ export declare function saveExternalSession(root: string, value: ExternalSessionRecord, previousDigest?: string): ExternalSessionSummary;
73
+ export declare function retainedExternalSession(root: string, producer: string, sessionId: string): ExternalSessionRecord | undefined;