mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
fix: let discussion peers see workspace file paths instead of [path]
Peer transcripts in natural discussions replaced every token that starts with "/" by [path]. Agents working on the same repository could not tell each other which file they changed, and API routes such as /api/users or commands such as /login were erased too. Natural discussion transcripts now show files inside the shared workspace as workspace-relative paths, keep non-filesystem tokens, and still redact other absolute local paths (/Users, /tmp, ...) and secrets. Phased-V4 delivery text keeps the previous redaction because its hashes take part in resume checks. Checks: collaboration-records (new peer path test), natural discussion (new end-to-end relative path test), natural transcript and delivery suites. Full desktop suite: 1537 pass.
This commit is contained in:
1 parent
b2249f0859
commit
8d8f9e65f6
4 files changed
+78
-3
No files matched your search
@@ -533,6 +533,37 @@ function publicCollaborationText(value, limit = MAX_TEXT_CHARS) {
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
const PRIVATE_FILESYSTEM_ROOT = /^\/(?:Users|home|private|var|tmp|etc|opt|Volumes|root|usr|System|Library|Applications|mnt|srv)(?:\/|$)/
|
||||
|
||||
function peerPathText(candidate, workspaceRoot) {
|
||||
const trailing = candidate.match(/[.,;:!?]+$/)?.[0] || ''
|
||||
const clean = trailing ? candidate.slice(0, -trailing.length) : candidate
|
||||
if (workspaceRoot && (clean === workspaceRoot || clean.startsWith(`${workspaceRoot}/`))) {
|
||||
return `${clean === workspaceRoot ? '.' : clean.slice(workspaceRoot.length + 1)}${trailing}`
|
||||
}
|
||||
if (PRIVATE_FILESYSTEM_ROOT.test(clean)) return `[path]${trailing}`
|
||||
// Routes and commands such as /api/users or /login are not local files.
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Peer-visible text for natural discussion transcripts. Unlike
|
||||
// publicCollaborationText, files inside the shared workspace stay readable as
|
||||
// workspace-relative paths and non-filesystem tokens (API routes, slash
|
||||
// commands) are kept; other absolute local paths and secrets stay redacted.
|
||||
function peerCollaborationText(value, options = {}) {
|
||||
const workspaceRoot = typeof options.workspaceRoot === 'string'
|
||||
? options.workspaceRoot.replace(/\/+$/, '')
|
||||
: ''
|
||||
return redactSecrets(String(value || ''))
|
||||
.replace(/\bfile:\/\/(\/[^\s"'`<>]+)/giu, (_match, filePath) => peerPathText(filePath, workspaceRoot))
|
||||
.replace(/(^|[\s("'`])(\/(?!\/)[^\s"'`<>)]*)/gmu, (_match, lead, candidate) => (
|
||||
`${lead}${peerPathText(candidate, workspaceRoot)}`
|
||||
))
|
||||
.replace(/\b[A-Za-z]:\\(?:[^\s"'`<>]+\\)*[^\s"'`<>]*/gu, '[path]')
|
||||
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function collaborationPackageText(handoff, entries) {
|
||||
const lines = [
|
||||
'MELDWORK_COLLABORATION_PACKAGE_V1',
|
||||
@@ -557,6 +588,7 @@ function collaborationPackageText(handoff, entries) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
peerCollaborationText,
|
||||
BLACKBOARD_ENTRY_VERSION,
|
||||
COLLABORATION_STATE_VERSION,
|
||||
HANDOFF_VERSION,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { createHash } = require('node:crypto')
|
||||
const path = require('node:path')
|
||||
const { MAX_MESSAGE_TEXT_CHARS } = require('./local-workspace-contracts.cjs')
|
||||
|
||||
const {
|
||||
@@ -21,6 +22,7 @@ const {
|
||||
collaborationPackageText,
|
||||
createBlackboardEntryRecord,
|
||||
emptyCollaborationState,
|
||||
peerCollaborationText,
|
||||
publicCollaborationText,
|
||||
roleForIndex,
|
||||
visibleBlackboardEntries,
|
||||
@@ -1711,11 +1713,13 @@ class LocalWorkspaceAutoRunner {
|
||||
}
|
||||
}
|
||||
|
||||
v4SanitizeDeliveryText(value, limit = 6000) {
|
||||
v4SanitizeDeliveryText(value, limit = 6000, options = {}) {
|
||||
const privateWrapper = '(?:private[ _-]?(?:reasoning|thoughts?)|analysis|chain[ _-]?of[ _-]?thought|cot)'
|
||||
const privateHeading = '(?:private[ _-]?(?:reasoning|thoughts?)|chain[ _-]?of[ _-]?thought|cot)'
|
||||
const stripped = this.v4StripMeldworkControls(
|
||||
publicCollaborationText(value, Number.MAX_SAFE_INTEGER),
|
||||
options.workspaceRoot
|
||||
? peerCollaborationText(value, { workspaceRoot: options.workspaceRoot })
|
||||
: publicCollaborationText(value, Number.MAX_SAFE_INTEGER),
|
||||
)
|
||||
const maximum = Number.isSafeInteger(limit) && limit >= 0 ? limit : 6000
|
||||
let privateSectionLevel = null
|
||||
@@ -2530,7 +2534,11 @@ class LocalWorkspaceAutoRunner {
|
||||
phase: message.trace?.phase || 'proposal',
|
||||
operationId: message.trace?.context?.operationId || '',
|
||||
})).map(entry => ({
|
||||
...entry, text: this.v4SanitizeDeliveryText(entry.text, Number.MAX_SAFE_INTEGER),
|
||||
// Peers collaborating on the same workspace must be able to name its
|
||||
// files; keep workspace paths readable as relative paths.
|
||||
...entry, text: this.v4SanitizeDeliveryText(entry.text, Number.MAX_SAFE_INTEGER, {
|
||||
workspaceRoot: group.workdir ? path.resolve(group.workdir) : '',
|
||||
}),
|
||||
}))
|
||||
return entries.map(entry => ({ ...entry, hash: hashValue(entry) }))
|
||||
}
|
||||
|
||||
@@ -138,3 +138,15 @@ test('conflicting claims append a conflict and remain selectively visible', () =
|
||||
})
|
||||
assert.deepEqual(parseCollaborationState(state), state)
|
||||
})
|
||||
|
||||
test('peer transcripts keep workspace files readable and other local paths private', () => {
|
||||
const { peerCollaborationText } = require('../../src/collaboration/collaboration-records.cjs')
|
||||
const text = peerCollaborationText(
|
||||
'Edited /work/proj/src/app.ts (see file:///work/proj/docs/a.md). /api/users still fails; '
|
||||
+ 'logs are in /Users/me/private.log and /tmp/run.log; run /login.',
|
||||
{ workspaceRoot: '/work/proj' },
|
||||
)
|
||||
assert.equal(text,
|
||||
'Edited src/app.ts (see docs/a.md). /api/users still fails; '
|
||||
+ 'logs are in [path] and [path]; run /login.')
|
||||
})
|
||||
@@ -161,6 +161,29 @@ test('Natural discussion accepts a fenced, loosely written owner receipt instead
|
||||
assert.equal(ledger.get(controller.runId).agentRuns.at(-1).context.taskDecision.status, 'completed')
|
||||
})
|
||||
|
||||
test('Natural discussion shows peers workspace files as relative paths', async t => {
|
||||
const { directory, options } = fixture()
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const prompts = []
|
||||
const workspace = new LocalWorkspace({
|
||||
...options, naturalAgentResponses: true,
|
||||
runAgent: async (agent, prompt) => {
|
||||
prompts.push({ kind: agent.kind, phase: naturalPhase(prompt), prompt })
|
||||
if (naturalPhase(prompt) === 'proposal') {
|
||||
return { outcome: 'completed', text: `${agent.kind} would change ${path.join(directory, 'src', 'app.ts')} and the /api/users route.` }
|
||||
}
|
||||
return { outcome: 'completed', text: decisionReply(`${agent.kind} reviewed the change.`) }
|
||||
},
|
||||
})
|
||||
await workspace.refreshAgents()
|
||||
const group = workspace.createGroup({ name: 'Paths', agentKinds: ['codex', 'hermes'], workdir: directory, allowWrite: false })
|
||||
await runDiscussion(workspace, group, { discussionStyle: 'agent-led', maxRounds: 3 })
|
||||
const discussion = prompts.find(item => item.phase === 'discussion' && item.kind === 'hermes')
|
||||
assert.ok(discussion)
|
||||
assert.match(discussion.prompt, /codex would change src\/app\.ts and the \/api\/users route\./)
|
||||
assert.doesNotMatch(discussion.prompt, /would change \[path\]/)
|
||||
})
|
||||
|
||||
test('Natural discussion hands a peer blocker to the delivery owner instead of ending', async t => {
|
||||
const { directory, options } = fixture()
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
|
||||
Reference in new issue
Block a user