@hanna84/mcp-writing 1.6.3 → 1.8.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.
- package/CHANGELOG.md +20 -0
- package/README.md +42 -5
- package/db.js +32 -1
- package/index.js +46 -2
- package/package.json +2 -1
- package/scripts/setup-openclaw-env.sh +64 -0
- package/sync.js +42 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,31 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v1.8.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v1.7.0...v1.8.0)
|
|
9
|
+
|
|
10
|
+
- feat(search): index metadata keywords in scenes FTS and preserve legacy index data [`#50`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/50)
|
|
12
|
+
|
|
13
|
+
#### [v1.7.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
|
+
/compare/v1.6.3...v1.7.0)
|
|
15
|
+
|
|
16
|
+
> 19 April 2026
|
|
17
|
+
|
|
18
|
+
- feat: configurable OpenClaw runtime identity and ownership guardrails [`#49`](https://github.com/hannasdev/mcp-writing.git
|
|
19
|
+
/pull/49)
|
|
20
|
+
- Release 1.7.0 [`f859eb5`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/f859eb57bcea412591635185e113f2c6467b0f59)
|
|
22
|
+
|
|
7
23
|
#### [v1.6.3](https://github.com/hannasdev/mcp-writing.git
|
|
8
24
|
/compare/v1.6.2...v1.6.3)
|
|
9
25
|
|
|
26
|
+
> 19 April 2026
|
|
27
|
+
|
|
10
28
|
- docs: sync PRD with current app behavior [`#48`](https://github.com/hannasdev/mcp-writing.git
|
|
11
29
|
/pull/48)
|
|
30
|
+
- Release 1.6.3 [`53add10`](https://github.com/hannasdev/mcp-writing.git
|
|
31
|
+
/commit/53add102551c586c81aaef249509eec2c5487f96)
|
|
12
32
|
|
|
13
33
|
#### [v1.6.2](https://github.com/hannasdev/mcp-writing.git
|
|
14
34
|
/compare/v1.6.1...v1.6.2)
|
package/README.md
CHANGED
|
@@ -315,7 +315,7 @@ Outcome: you get AI speed with explicit approval and recoverable history for eve
|
|
|
315
315
|
| `list_places` | All places |
|
|
316
316
|
| `get_place_sheet` | Full place metadata, tags, associated characters, notes, and support notes |
|
|
317
317
|
| `create_place_sheet` | Create a canonical place sheet folder and sidecar |
|
|
318
|
-
| `search_metadata` | Full-text search across scene titles and
|
|
318
|
+
| `search_metadata` | Full-text search across scene titles, loglines, and metadata keywords (tags/characters/places/versions) |
|
|
319
319
|
| `list_threads` | All subplot threads for a project |
|
|
320
320
|
| `get_thread_arc` | Scenes belonging to a thread, with per-thread beat |
|
|
321
321
|
| `upsert_thread_link` | Create/update a thread and link it to a scene |
|
|
@@ -339,13 +339,16 @@ Paginated tools (`find_scenes`, `get_arc`, `list_threads`, `get_thread_arc`, `se
|
|
|
339
339
|
# docker-compose.yml snippet
|
|
340
340
|
writing-mcp:
|
|
341
341
|
build: .
|
|
342
|
-
user: "${
|
|
342
|
+
user: "${OPENCLAW_UID:-1000}:${OPENCLAW_GID:-1000}"
|
|
343
343
|
environment:
|
|
344
344
|
WRITING_SYNC_DIR: /sync
|
|
345
345
|
DB_PATH: /data/writing.db
|
|
346
346
|
HTTP_PORT: "3000"
|
|
347
|
+
OWNERSHIP_GUARD_MODE: "${OWNERSHIP_GUARD_MODE:-warn}"
|
|
348
|
+
GIT_SSH_COMMAND: "ssh -i /ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/ssh/known_hosts"
|
|
347
349
|
volumes:
|
|
348
|
-
- /
|
|
350
|
+
- ${OPENCLAW_WORKSPACE_DIR:?run scripts/setup-openclaw-env.sh first}/sync:/sync
|
|
351
|
+
- ${OPENCLAW_SSH_DIR:?run scripts/setup-openclaw-env.sh first}:/ssh:ro
|
|
349
352
|
- writing-mcp-data:/data
|
|
350
353
|
healthcheck:
|
|
351
354
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
@@ -357,7 +360,15 @@ volumes:
|
|
|
357
360
|
writing-mcp-data:
|
|
358
361
|
```
|
|
359
362
|
|
|
360
|
-
|
|
363
|
+
Recommended: start from `docker-compose.example.yml` and generate `.env` with machine-specific values:
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
sh scripts/setup-openclaw-env.sh
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
That script writes `OPENCLAW_UID`, `OPENCLAW_GID`, `OPENCLAW_WORKSPACE_DIR`, and `OPENCLAW_SSH_DIR` to `.env`.
|
|
370
|
+
Running Compose without these values is unsupported and may create invalid mount definitions.
|
|
371
|
+
It also normalizes `OWNERSHIP_GUARD_MODE` to `warn` or `fail` and preserves an existing valid value when rerun.
|
|
361
372
|
|
|
362
373
|
Then register in your OpenClaw config:
|
|
363
374
|
|
|
@@ -380,8 +391,17 @@ When `mcp-writing` runs behind OpenClaw (or any Docker MCP gateway), these detai
|
|
|
380
391
|
|
|
381
392
|
- Set `WRITING_SYNC_DIR=/sync`
|
|
382
393
|
- Set `DB_PATH=/data/writing.db`
|
|
394
|
+
- Set `OWNERSHIP_GUARD_MODE=warn` (or `fail` to block startup on ownership drift)
|
|
383
395
|
- Mount your manuscript sync repo to `/sync`
|
|
384
396
|
- Mount a persistent path for SQLite data at `/data`
|
|
397
|
+
- Mount SSH materials read-only at `/ssh` and use `GIT_SSH_COMMAND` with `/ssh` paths
|
|
398
|
+
|
|
399
|
+
Debug/test-only runtime override knobs:
|
|
400
|
+
|
|
401
|
+
- `RUNTIME_UID_OVERRIDE` — test helper to simulate runtime UID during ownership diagnostics
|
|
402
|
+
- `ALLOW_RUNTIME_UID_OVERRIDE=1` — explicitly enables the override outside `NODE_ENV=test`
|
|
403
|
+
|
|
404
|
+
Do not set these in normal production or desktop deployments.
|
|
385
405
|
|
|
386
406
|
If `/sync` contains raw Scrivener external-sync output, run the importer once before normal `sync` usage:
|
|
387
407
|
|
|
@@ -414,7 +434,7 @@ For private remotes, mount SSH materials read-only and enforce strict host check
|
|
|
414
434
|
Example:
|
|
415
435
|
|
|
416
436
|
```sh
|
|
417
|
-
export GIT_SSH_COMMAND="ssh -i /
|
|
437
|
+
export GIT_SSH_COMMAND="ssh -i /ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/ssh/known_hosts"
|
|
418
438
|
```
|
|
419
439
|
|
|
420
440
|
#### Separate auth and signing keys
|
|
@@ -603,6 +623,22 @@ git config --system --add safe.directory /sync
|
|
|
603
623
|
4. Verify SSH key has write access to the remote and `known_hosts` is mounted.
|
|
604
624
|
5. Prefer branch-per-change workflow (`bot/*` or `edda/*`) if `main` is protected.
|
|
605
625
|
|
|
626
|
+
### "Blocked: file is root-owned" (EACCES / ownership drift)
|
|
627
|
+
|
|
628
|
+
The runtime user can read but cannot overwrite prose files.
|
|
629
|
+
|
|
630
|
+
Fix:
|
|
631
|
+
|
|
632
|
+
1. Repair host ownership once:
|
|
633
|
+
|
|
634
|
+
```sh
|
|
635
|
+
sudo chown -R "$(id -u):$(id -g)" /path/to/sync-dir
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
2. Ensure container user mapping is set from `.env` (`OPENCLAW_UID` / `OPENCLAW_GID`).
|
|
639
|
+
3. Optionally set `OWNERSHIP_GUARD_MODE=fail` to catch mismatches at startup.
|
|
640
|
+
4. Re-check `get_runtime_config` and confirm ownership warnings are gone.
|
|
641
|
+
|
|
606
642
|
### Tests fail after updating Node.js
|
|
607
643
|
|
|
608
644
|
Local install state may be stale after the Node.js change.
|
|
@@ -624,6 +660,7 @@ npm test
|
|
|
624
660
|
| `HTTP_PORT` | `3000` | Port for the MCP SSE endpoint |
|
|
625
661
|
| `MAX_CHAPTER_SCENES` | `10` | Maximum scenes returned by `get_chapter_prose` |
|
|
626
662
|
| `DEFAULT_METADATA_PAGE_SIZE` | `20` | Default page size for paginated tools |
|
|
663
|
+
| `OWNERSHIP_GUARD_MODE` | `warn` | Startup ownership policy: `warn` logs drift, `fail` exits when sampled files are not owned by runtime user |
|
|
627
664
|
|
|
628
665
|
## License
|
|
629
666
|
|
package/db.js
CHANGED
|
@@ -116,12 +116,43 @@ export const SCHEMA = `
|
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
CREATE VIRTUAL TABLE IF NOT EXISTS scenes_fts USING fts5(
|
|
119
|
-
scene_id, project_id, logline, title
|
|
119
|
+
scene_id, project_id, logline, title, keywords
|
|
120
120
|
);
|
|
121
121
|
`;
|
|
122
122
|
|
|
123
123
|
export function openDb(dbPath) {
|
|
124
124
|
const db = new DatabaseSync(dbPath);
|
|
125
125
|
db.exec(SCHEMA);
|
|
126
|
+
|
|
127
|
+
// Rebuild legacy FTS table if it predates keyword indexing.
|
|
128
|
+
// Preserve existing indexed rows so metadata search remains available
|
|
129
|
+
// even before the next sync pass repopulates from source files.
|
|
130
|
+
const ftsSql = db.prepare(`
|
|
131
|
+
SELECT sql
|
|
132
|
+
FROM sqlite_master
|
|
133
|
+
WHERE type = 'table' AND name = 'scenes_fts'
|
|
134
|
+
`).get()?.sql;
|
|
135
|
+
if (typeof ftsSql === "string" && !ftsSql.toLowerCase().includes("keywords")) {
|
|
136
|
+
db.exec(`BEGIN IMMEDIATE;`);
|
|
137
|
+
try {
|
|
138
|
+
db.exec(`
|
|
139
|
+
CREATE VIRTUAL TABLE scenes_fts_migrating USING fts5(
|
|
140
|
+
scene_id, project_id, logline, title, keywords
|
|
141
|
+
);
|
|
142
|
+
`);
|
|
143
|
+
db.exec(`
|
|
144
|
+
INSERT INTO scenes_fts_migrating (scene_id, project_id, logline, title, keywords)
|
|
145
|
+
SELECT scene_id, project_id, logline, title, ''
|
|
146
|
+
FROM scenes_fts;
|
|
147
|
+
`);
|
|
148
|
+
db.exec(`DROP TABLE scenes_fts;`);
|
|
149
|
+
db.exec(`ALTER TABLE scenes_fts_migrating RENAME TO scenes_fts;`);
|
|
150
|
+
db.exec(`COMMIT;`);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
db.exec(`ROLLBACK;`);
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
return db;
|
|
127
158
|
}
|
package/index.js
CHANGED
|
@@ -19,6 +19,11 @@ const DB_PATH_DISPLAY = DB_PATH === ":memory:" ? DB_PATH : path.resolve(DB_PATH)
|
|
|
19
19
|
const HTTP_PORT = parseInt(process.env.HTTP_PORT ?? "3000", 10);
|
|
20
20
|
const MAX_CHAPTER_SCENES = parseInt(process.env.MAX_CHAPTER_SCENES ?? "10", 10);
|
|
21
21
|
const DEFAULT_METADATA_PAGE_SIZE = parseInt(process.env.DEFAULT_METADATA_PAGE_SIZE ?? "20", 10);
|
|
22
|
+
const OWNERSHIP_GUARD_MODE_RAW = (process.env.OWNERSHIP_GUARD_MODE ?? "warn").trim().toLowerCase();
|
|
23
|
+
const OWNERSHIP_GUARD_MODE = OWNERSHIP_GUARD_MODE_RAW === "fail" || OWNERSHIP_GUARD_MODE_RAW === "warn"
|
|
24
|
+
? OWNERSHIP_GUARD_MODE_RAW
|
|
25
|
+
: "warn";
|
|
26
|
+
const OWNERSHIP_GUARD_MODE_RAW_DISPLAY = JSON.stringify(OWNERSHIP_GUARD_MODE_RAW);
|
|
22
27
|
|
|
23
28
|
function paginateRows(rows, { page, pageSize, forcePagination = false }) {
|
|
24
29
|
const totalCount = rows.length;
|
|
@@ -302,6 +307,23 @@ function getRuntimeDiagnostics() {
|
|
|
302
307
|
const warnings = [];
|
|
303
308
|
const recommendations = [];
|
|
304
309
|
|
|
310
|
+
if (OWNERSHIP_GUARD_MODE_RAW !== OWNERSHIP_GUARD_MODE) {
|
|
311
|
+
warnings.push(
|
|
312
|
+
`OWNERSHIP_GUARD_MODE_INVALID: Unsupported OWNERSHIP_GUARD_MODE=${OWNERSHIP_GUARD_MODE_RAW_DISPLAY}. Falling back to 'warn'.`
|
|
313
|
+
);
|
|
314
|
+
recommendations.push("Set OWNERSHIP_GUARD_MODE to either 'warn' or 'fail'.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid_override_ignored) {
|
|
318
|
+
warnings.push("RUNTIME_UID_OVERRIDE_IGNORED: RUNTIME_UID_OVERRIDE is ignored unless NODE_ENV=test or ALLOW_RUNTIME_UID_OVERRIDE=1.");
|
|
319
|
+
recommendations.push("Avoid RUNTIME_UID_OVERRIDE in production runtime environments.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid_override_invalid) {
|
|
323
|
+
warnings.push("RUNTIME_UID_OVERRIDE_INVALID: RUNTIME_UID_OVERRIDE must be a non-negative integer when enabled.");
|
|
324
|
+
recommendations.push("Set RUNTIME_UID_OVERRIDE to a non-negative integer, or unset it.");
|
|
325
|
+
}
|
|
326
|
+
|
|
305
327
|
if (!SYNC_DIR_WRITABLE) {
|
|
306
328
|
warnings.push("SYNC_DIR_READ_ONLY: sync dir is read-only; metadata write-back and prose editing tools are unavailable.");
|
|
307
329
|
recommendations.push("Mount WRITING_SYNC_DIR with write access (avoid read-only mounts like ':ro').");
|
|
@@ -316,10 +338,17 @@ function getRuntimeDiagnostics() {
|
|
|
316
338
|
`Repair ownership once on host: sudo chown -R "$(id -u):$(id -g)" "${SYNC_DIR_ABS}"`
|
|
317
339
|
);
|
|
318
340
|
recommendations.push(
|
|
319
|
-
"For Docker, run container as host user (compose: user: \"${
|
|
341
|
+
"For Docker/OpenClaw, run container as host user (compose: user: \"${OPENCLAW_UID:-1000}:${OPENCLAW_GID:-1000}\")."
|
|
320
342
|
);
|
|
321
343
|
}
|
|
322
344
|
|
|
345
|
+
if (OWNERSHIP_GUARD_MODE === "fail" && SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid === 0) {
|
|
346
|
+
warnings.push(
|
|
347
|
+
"OWNERSHIP_GUARD_SKIPPED_FOR_ROOT: OWNERSHIP_GUARD_MODE=fail is skipped because runtime UID is 0 (root)."
|
|
348
|
+
);
|
|
349
|
+
recommendations.push("Prefer running as a non-root host-mapped UID/GID to make ownership guard checks meaningful.");
|
|
350
|
+
}
|
|
351
|
+
|
|
323
352
|
if (SYNC_OWNERSHIP_DIAGNOSTICS.supported && SYNC_OWNERSHIP_DIAGNOSTICS.root_owned_paths > 0) {
|
|
324
353
|
warnings.push(
|
|
325
354
|
`ROOT_OWNED_PATHS: ${SYNC_OWNERSHIP_DIAGNOSTICS.root_owned_paths} sampled path(s) are owned by UID 0 (root).`
|
|
@@ -353,6 +382,20 @@ if (RUNTIME_DIAGNOSTICS.warnings.length) {
|
|
|
353
382
|
}
|
|
354
383
|
}
|
|
355
384
|
|
|
385
|
+
const SHOULD_ENFORCE_OWNERSHIP_FAIL_GUARD = OWNERSHIP_GUARD_MODE === "fail"
|
|
386
|
+
&& SYNC_OWNERSHIP_DIAGNOSTICS.supported
|
|
387
|
+
&& SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid !== 0;
|
|
388
|
+
|
|
389
|
+
if (SHOULD_ENFORCE_OWNERSHIP_FAIL_GUARD && SYNC_OWNERSHIP_DIAGNOSTICS.non_runtime_owned_paths > 0) {
|
|
390
|
+
process.stderr.write(
|
|
391
|
+
`[mcp-writing] FATAL: OWNERSHIP_GUARD_MODE=fail and ${SYNC_OWNERSHIP_DIAGNOSTICS.non_runtime_owned_paths} sampled path(s) are not owned by runtime UID ${SYNC_OWNERSHIP_DIAGNOSTICS.runtime_uid}.\n`
|
|
392
|
+
);
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
`[mcp-writing] FATAL: Repair ownership once on the host directory mounted at ${SYNC_DIR_ABS}: sudo chown -R "$(id -u):$(id -g)" /path/to/host-sync-dir\n`
|
|
395
|
+
);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
356
399
|
// Run sync on startup
|
|
357
400
|
syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
|
|
358
401
|
|
|
@@ -466,6 +509,7 @@ function createMcpServer() {
|
|
|
466
509
|
sync_dir: SYNC_DIR_ABS,
|
|
467
510
|
db_path: DB_PATH_DISPLAY,
|
|
468
511
|
sync_dir_writable: SYNC_DIR_WRITABLE,
|
|
512
|
+
ownership_guard_mode: OWNERSHIP_GUARD_MODE,
|
|
469
513
|
permission_diagnostics: SYNC_OWNERSHIP_DIAGNOSTICS,
|
|
470
514
|
git_available: GIT_AVAILABLE,
|
|
471
515
|
git_enabled: GIT_ENABLED,
|
|
@@ -902,7 +946,7 @@ function createMcpServer() {
|
|
|
902
946
|
// ---- search_metadata -----------------------------------------------------
|
|
903
947
|
s.tool(
|
|
904
948
|
"search_metadata",
|
|
905
|
-
"Full-text search across scene titles
|
|
949
|
+
"Full-text search across scene titles, loglines (synopsis/logline text fields), and metadata keywords (tags/characters/places/versions). Use this when you don't know the exact scene_id or chapter but want to find scenes by topic, theme, or metadata keyword. Not a prose search — use get_scene_prose to read actual text. Supports pagination via page/page_size and auto-paginates large result sets with total_count.",
|
|
906
950
|
{
|
|
907
951
|
query: z.string().describe("Search terms (e.g. 'hospital' or 'Sebastian feeding'). FTS5 syntax supported."),
|
|
908
952
|
page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanna84/mcp-writing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node --experimental-sqlite index.js",
|
|
23
23
|
"new:entity": "node scripts/new-world-entity.js",
|
|
24
|
+
"setup:openclaw-env": "sh scripts/setup-openclaw-env.sh",
|
|
24
25
|
"release": "release-it",
|
|
25
26
|
"lint": "eslint index.js importer.js db.js sync.js metadata-lint.js scripts/",
|
|
26
27
|
"lint:metadata": "node scripts/lint-metadata.mjs",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
ENV_FILE="${1:-.env}"
|
|
5
|
+
OPENCLAW_UID_VALUE="$(id -u)"
|
|
6
|
+
OPENCLAW_GID_VALUE="$(id -g)"
|
|
7
|
+
OPENCLAW_WORKSPACE_DIR_VALUE="${OPENCLAW_WORKSPACE_DIR:-$(pwd)}"
|
|
8
|
+
OPENCLAW_SSH_DIR_VALUE="${OPENCLAW_SSH_DIR:-${HOME:-$(pwd)}/.ssh}"
|
|
9
|
+
OWNERSHIP_GUARD_MODE_VALUE="${2:-${OWNERSHIP_GUARD_MODE:-}}"
|
|
10
|
+
|
|
11
|
+
quote_env_value() {
|
|
12
|
+
printf '"%s"' "$(printf '%s' "$1" | sed 's/["\\]/\\&/g')"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if [ -z "$OWNERSHIP_GUARD_MODE_VALUE" ] && [ -f "$ENV_FILE" ]; then
|
|
16
|
+
EXISTING_GUARD_MODE="$(grep -E '^OWNERSHIP_GUARD_MODE=' "$ENV_FILE" | tail -n1 | cut -d= -f2- || true)"
|
|
17
|
+
OWNERSHIP_GUARD_MODE_VALUE="$EXISTING_GUARD_MODE"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [ -z "$OWNERSHIP_GUARD_MODE_VALUE" ]; then
|
|
21
|
+
OWNERSHIP_GUARD_MODE_VALUE="warn"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
NORMALIZED_GUARD_MODE="$(printf '%s' "$OWNERSHIP_GUARD_MODE_VALUE" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
25
|
+
if [ "$NORMALIZED_GUARD_MODE" != "warn" ] && [ "$NORMALIZED_GUARD_MODE" != "fail" ]; then
|
|
26
|
+
printf 'Warning: unsupported OWNERSHIP_GUARD_MODE=%s, defaulting to "warn".\n' "$OWNERSHIP_GUARD_MODE_VALUE" >&2
|
|
27
|
+
OWNERSHIP_GUARD_MODE_VALUE="warn"
|
|
28
|
+
else
|
|
29
|
+
OWNERSHIP_GUARD_MODE_VALUE="$NORMALIZED_GUARD_MODE"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if TMP_FILE="$(mktemp -t openclaw-env.XXXXXX 2>/dev/null)"; then
|
|
33
|
+
:
|
|
34
|
+
else
|
|
35
|
+
TMP_FILE="$(mktemp "${TMPDIR:-/tmp}/openclaw-env.XXXXXX")"
|
|
36
|
+
fi
|
|
37
|
+
trap 'rm -f "$TMP_FILE"' EXIT
|
|
38
|
+
|
|
39
|
+
if [ -f "$ENV_FILE" ]; then
|
|
40
|
+
awk '
|
|
41
|
+
!/^OPENCLAW_UID=/ &&
|
|
42
|
+
!/^OPENCLAW_GID=/ &&
|
|
43
|
+
!/^OPENCLAW_WORKSPACE_DIR=/ &&
|
|
44
|
+
!/^OPENCLAW_SSH_DIR=/ &&
|
|
45
|
+
!/^OWNERSHIP_GUARD_MODE=/
|
|
46
|
+
' "$ENV_FILE" > "$TMP_FILE"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
cat "$TMP_FILE"
|
|
51
|
+
if [ -s "$TMP_FILE" ]; then
|
|
52
|
+
printf "\n"
|
|
53
|
+
fi
|
|
54
|
+
printf "OPENCLAW_UID=%s\n" "$OPENCLAW_UID_VALUE"
|
|
55
|
+
printf "OPENCLAW_GID=%s\n" "$OPENCLAW_GID_VALUE"
|
|
56
|
+
printf "OPENCLAW_WORKSPACE_DIR=%s\n" "$(quote_env_value "$OPENCLAW_WORKSPACE_DIR_VALUE")"
|
|
57
|
+
printf "OPENCLAW_SSH_DIR=%s\n" "$(quote_env_value "$OPENCLAW_SSH_DIR_VALUE")"
|
|
58
|
+
printf "OWNERSHIP_GUARD_MODE=%s\n" "$OWNERSHIP_GUARD_MODE_VALUE"
|
|
59
|
+
} > "$ENV_FILE"
|
|
60
|
+
|
|
61
|
+
printf "Wrote %s with OPENCLAW runtime variables.\n" "$ENV_FILE"
|
|
62
|
+
printf "UID:GID=%s:%s\n" "$OPENCLAW_UID_VALUE" "$OPENCLAW_GID_VALUE"
|
|
63
|
+
printf "WORKSPACE=%s\n" "$OPENCLAW_WORKSPACE_DIR_VALUE"
|
|
64
|
+
printf "SSH_DIR=%s\n" "$OPENCLAW_SSH_DIR_VALUE"
|
package/sync.js
CHANGED
|
@@ -241,7 +241,26 @@ function collectOwnershipSample(rootDir, limit = 200) {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
export function getSyncOwnershipDiagnostics(syncDir, { sampleLimit = 200 } = {}) {
|
|
244
|
-
|
|
244
|
+
let runtimeUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
245
|
+
const runtimeUidOverrideRaw = process.env.RUNTIME_UID_OVERRIDE;
|
|
246
|
+
const runtimeUidOverrideAllowed = process.env.NODE_ENV === "test" || process.env.ALLOW_RUNTIME_UID_OVERRIDE === "1";
|
|
247
|
+
let runtimeUidOverrideApplied = false;
|
|
248
|
+
let runtimeUidOverrideIgnored = false;
|
|
249
|
+
let runtimeUidOverrideInvalid = false;
|
|
250
|
+
|
|
251
|
+
if (runtimeUidOverrideRaw !== undefined) {
|
|
252
|
+
if (!runtimeUidOverrideAllowed) {
|
|
253
|
+
runtimeUidOverrideIgnored = true;
|
|
254
|
+
} else {
|
|
255
|
+
const parsed = Number.parseInt(runtimeUidOverrideRaw, 10);
|
|
256
|
+
if (Number.isInteger(parsed) && parsed >= 0) {
|
|
257
|
+
runtimeUid = parsed;
|
|
258
|
+
runtimeUidOverrideApplied = true;
|
|
259
|
+
} else {
|
|
260
|
+
runtimeUidOverrideInvalid = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
245
264
|
let syncDirPathExists;
|
|
246
265
|
let syncDirIsDirectory;
|
|
247
266
|
try {
|
|
@@ -261,6 +280,10 @@ export function getSyncOwnershipDiagnostics(syncDir, { sampleLimit = 200 } = {})
|
|
|
261
280
|
sync_dir_exists: syncDirIsDirectory,
|
|
262
281
|
supported: runtimeUid !== null,
|
|
263
282
|
runtime_uid: runtimeUid,
|
|
283
|
+
runtime_uid_override_requested: runtimeUidOverrideRaw !== undefined,
|
|
284
|
+
runtime_uid_override_applied: runtimeUidOverrideApplied,
|
|
285
|
+
runtime_uid_override_ignored: runtimeUidOverrideIgnored,
|
|
286
|
+
runtime_uid_override_invalid: runtimeUidOverrideInvalid,
|
|
264
287
|
sampled_paths: 0,
|
|
265
288
|
sample_limit: sampleLimit,
|
|
266
289
|
root_owned_paths: 0,
|
|
@@ -492,8 +515,24 @@ export function indexSceneFile(db, syncDir, file, meta, prose) {
|
|
|
492
515
|
);
|
|
493
516
|
}
|
|
494
517
|
|
|
495
|
-
|
|
496
|
-
meta.
|
|
518
|
+
const keywordTokens = [
|
|
519
|
+
...(meta.tags ?? []),
|
|
520
|
+
...(meta.characters ?? []),
|
|
521
|
+
...(meta.places ?? []),
|
|
522
|
+
...(meta.versions ?? []),
|
|
523
|
+
]
|
|
524
|
+
.filter(Boolean)
|
|
525
|
+
.map(String)
|
|
526
|
+
.map(s => s.trim())
|
|
527
|
+
.filter(Boolean)
|
|
528
|
+
.join(" ");
|
|
529
|
+
|
|
530
|
+
db.prepare(`INSERT OR REPLACE INTO scenes_fts (scene_id, project_id, logline, title, keywords) VALUES (?, ?, ?, ?, ?)`).run(
|
|
531
|
+
meta.scene_id,
|
|
532
|
+
project_id,
|
|
533
|
+
meta.logline ?? meta.synopsis ?? "",
|
|
534
|
+
meta.title ?? "",
|
|
535
|
+
keywordTokens,
|
|
497
536
|
);
|
|
498
537
|
|
|
499
538
|
return { isStale };
|