drafted 1.19.11 → 1.19.16
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.
- package/mcp/server.mjs +24 -1
- package/mcp/test-org-guards.mjs +11 -1
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -198,6 +198,17 @@ function scrubLocalPathMentions(description) {
|
|
|
198
198
|
// inside the factory so each HTTP request gets its own isolated server.
|
|
199
199
|
// Stdio mode uses the `mcpServer` singleton (built once at module load).
|
|
200
200
|
|
|
201
|
+
// What an fs(rm) on a project path actually targets. A layer or lane path carries
|
|
202
|
+
// no filename, so before this predicate existed it fell through to the "no file
|
|
203
|
+
// path = archive the project" branch and archived the WHOLE project — returning a
|
|
204
|
+
// normal-looking success. That is how "Ai For Coaches Talk" was lost to an
|
|
205
|
+
// fs(rm) on /slides/deck. Only the bare project path may archive.
|
|
206
|
+
export function rmScope(layer, lane, filename) {
|
|
207
|
+
if (filename) return 'file';
|
|
208
|
+
if (layer || lane) return 'directory';
|
|
209
|
+
return 'project';
|
|
210
|
+
}
|
|
211
|
+
|
|
201
212
|
// ── Org-ambiguity policy (the one decision core) ─────────────────
|
|
202
213
|
// The org guard inside the factory plumbs session/HTTP state into this
|
|
203
214
|
// side-effect-free predicate, which IS the policy (DRAFT-36 "one rule"). Top-level
|
|
@@ -3615,7 +3626,19 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3615
3626
|
// __archived folder. Frame/lane rm still deletes those items, but a
|
|
3616
3627
|
// whole project is never hard-deleted by an agent — the web UI's
|
|
3617
3628
|
// Archive bin is where a human permanently deletes.
|
|
3618
|
-
|
|
3629
|
+
// A layer or lane path has no filename, so it used to fall through to
|
|
3630
|
+
// the archive branch and archive the WHOLE project — with a
|
|
3631
|
+
// success-shaped response. /slides/deck and /slides are one keystroke
|
|
3632
|
+
// from the project path; only the bare project path may archive.
|
|
3633
|
+
const scope = rmScope(layer, lane, filename);
|
|
3634
|
+
if (scope === 'directory') {
|
|
3635
|
+
return err(new Error(
|
|
3636
|
+
`rm on a ${lane ? 'lane' : 'layer'} path deletes nothing — Drafted has no directory delete. ` +
|
|
3637
|
+
`Remove the frames individually: /projects/<project>/${layer}${lane ? '/' + lane : ''}/<file>. ` +
|
|
3638
|
+
`To archive the whole project, address the project itself: /projects/<project>`
|
|
3639
|
+
));
|
|
3640
|
+
}
|
|
3641
|
+
if (scope === 'project') {
|
|
3619
3642
|
const pid = getState().projectId;
|
|
3620
3643
|
if (!pid) return err(new Error('could not resolve project id for archive'));
|
|
3621
3644
|
const result = await api('PATCH', `/api/project/${pid}`, { folder: '__archived' }, orgHeader);
|
package/mcp/test-org-guards.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import assert from 'node:assert/strict';
|
|
|
5
5
|
import { mkdtempSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { projectlessMutationNeedsOrg, boundOrgRejected, receiptOrg, splitOrgScope, stripUrlOrigin } from './server.mjs';
|
|
8
|
+
import { projectlessMutationNeedsOrg, boundOrgRejected, receiptOrg, splitOrgScope, stripUrlOrigin, rmScope } from './server.mjs';
|
|
9
9
|
import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
|
|
10
10
|
|
|
11
11
|
// One rule governs create AND fork (a fork is a create). A write proceeds when its
|
|
@@ -168,6 +168,16 @@ assert.equal(stripUrlOrigin('/o/acme/wiki/x'), '/o/acme/wiki/x', 'plain paths pa
|
|
|
168
168
|
assert.equal(stripUrlOrigin('not a url'), 'not a url', 'non-URL input passes through');
|
|
169
169
|
assert.equal(stripUrlOrigin('/f/00000000-0000-0000-0000-000000000000'), '/f/00000000-0000-0000-0000-000000000000', '/f/ frame links pass through');
|
|
170
170
|
|
|
171
|
+
// fs(rm) scope: only a BARE project path may archive a project. A lane path
|
|
172
|
+
// (/slides/deck) and a layer path (/slides) both parse with filename=null and
|
|
173
|
+
// used to fall through to the archive branch, silently archiving the whole
|
|
174
|
+
// project with a success-shaped response.
|
|
175
|
+
assert.equal(rmScope('slides', 'deck', '12.html'), 'file', 'a full frame path removes that frame');
|
|
176
|
+
assert.equal(rmScope('slides', null, '12.html'), 'file', 'a layer-root file path removes that frame');
|
|
177
|
+
assert.equal(rmScope('slides', 'deck', null), 'directory', 'a LANE path must never archive the project');
|
|
178
|
+
assert.equal(rmScope('slides', null, null), 'directory', 'a LAYER path must never archive the project');
|
|
179
|
+
assert.equal(rmScope(null, null, null), 'project', 'only the bare project path archives the project');
|
|
180
|
+
|
|
171
181
|
console.log('org-guard policy OK');
|
|
172
182
|
// Importing server.mjs builds the stdio MCP singleton, which opens a WS reconnect
|
|
173
183
|
// loop that keeps the event loop alive. Assertions are done — exit deterministically.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.16",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|