Skip to content

chore(skill-lib): refresh root skill-lib snapshot to 1b1a947 (#120) - #78

Open
erinepshovel-code wants to merge 16 commits into
mainfrom
sync/skill-lib-9867ab3
Open

erinepshovel-code wants to merge 16 commits into
mainfrom
sync/skill-lib-9867ab3

Conversation

@erinepshovel-code

@erinepshovel-code erinepshovel-code commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR refreshes the complete root skill-lib/ snapshot to The-Interdependency/skill-lib@1b1a9473fcbaab16965af9907e4aafd12853a704 (main after skill-lib #120). It also moves stack's MSDMD fresh-making adapter onto the new collector. Erin approved the refresh on 2026-09-28 and the follow-ups on 2026-10-06.

  • Old pin (main): fb3b53a7629f7f03ecf255167d52c13abef1a979 (plus licensing-only files from e6e3e5c)
  • Intermediate pin: 38c64332b840b2bbe1c07e53aeee8996644548e9 (after skill-lib #118)
  • New pin: 1b1a9473fcbaab16965af9907e4aafd12853a704 (after skill-lib #120: Node --jitless, signal/probe exit 3)
  • Commits:
    • f55bda8 mirrored 9867ab3.
    • 131497c re-mirrors 38c6433 (3 files added, 12 modified, 0 removed versus 9867ab3). It also moves every pin and changes the backend.
    • f318657 re-pins to 1b1a947 (1 added, 7 modified, 0 removed versus 38c6433). Blob- and mode-identical to git archive 1b1a947. Pins and work_graph_sha256 updated (48ee1761… → da8104f0…). Compare 38c6433..1b1a947 (fresh-making/SKILL.md unchanged; msdmd generator changed).
    • 8cb6126 merges stack main (weave feat(weave): complete native sequence cycle and repair review findings #77 / ccf707f) into this branch. Keeps the 1b1a947 skill-lib pin; adds weave's ucns/uchc research participants; takes main's updated weave boundary hmmm; recomputes work_graph_sha256 (6340a86d…).
    • ae8677e restores main's weave/stack research-participant relation text (merge had kept the pre-feat(weave): complete native sequence cycle and repair review findings #77 wording); recomputes work_graph_sha256 (7e2aeaee…).
    • After all, skill-lib/ is blob- and mode-identical to git archive 1b1a947, with no stack-local residue.
  • Branch name: it is still sync/skill-lib-9867ab3. I kept it on purpose so this PR stays the same; the content is pinned to 1b1a947.

Review fixes (Review on 131497c), as normal commits with tests

Commit Finding Fix
770857e P1: python -m puts cwd ahead of PYTHONPATH, and the collector ran with cwd at the target, so a target's msdmd/collect.py (or yaml.py) shadowed the pinned collector. A forged artifact got published as fresh. The executor, the verifier and --print-generator-identity all run with cwd=generator_root and python -P. The docstring is fixed. Test: a target with its own msdmd/ package and yaml.py never runs, and its bytes never publish. I checked the real collector on pcea c24c14b: the output is byte-identical to cwd=target (3b3eefc6…).
ad7de7a P2: the identity was computed in the operator shell. The VM_SETUP queue-from-shell flow re-keyed jobs, and so did fresh status. Only the environment that runs the collector observes the identity. --queue-only, fresh status and fresh explain never probe it. They keep the identity the worker recorded, or worker-pending for a new derivation. The worker records its identity plus its components (--print-generator-identity --json). It supersedes a worker-pending job once, under the key it observed. Any later move still fails closed. Mismatches name the components that differ, for example node: v24.15.0 -> absent. New backend/ops/worker_sandbox_run.sh runs a command through systemd-run with the worker unit's User, EnvironmentFile and sandbox properties. VM_SETUP's queue/status flow uses it now. New contracts stack_msdmd_target_cannot_shadow_collector and stack_msdmd_identity_observed_where_executed, each with a check.
3c0dd5f P2: install_msdmd_runtime.sh didn't fail when node or typescript was absent. P3: umask and the worker user. The script exits 3 if the probe fails or reports node or typescript as absent. It also probes inside the worker unit's sandbox through worker_sandbox_run.sh, and exits 4 without root or systemd-run unless STACK_SANDBOX_PROBE=skip. It sets umask 022 and requires STACK_WORKER_USER (default stackorchestrator) to exist. Tests stub pip, npm and the collector (backend/tests/test_install_script.py).
5c85551 P3: unmapped exits and signals logged only stderr. P3: an OSError on spawn crashed the worker loop. collector_failure now names exit N or killed by SIGKILL/SIGSEGV, and signal deaths get an operator hmmm. A spawn OSError (for example ENOMEM) in the executor or verifier is recorded with ledger.fail. The identity probe and the git HEAD lookup fail closed. evaluate reports verifier-unavailable. worker.run_once records any unexpected exception on the job and keeps looping, but still crashes so systemd restarts it if the ledger is unreachable.
8a8e1c3 P3: stale candidate and rollback files, and a failed git status treated as clean. Before the dirty check, run_job removes our own hidden .candidate, .verify, .accepted-backup and .fresh-status-verify siblings that are older than the lease. It only touches regular files with our names. A stale rollback that still holds the accepted bytes, while the artifact doesn't, is restored instead of deleted. A git status that can't run or exits nonzero is now a hold.
af58b11 No CI check ran backend/tests. New .github/workflows/backend.yml runs bash -n backend/ops/*.sh and unittest discover -s backend/tests on Python 3.11 when backend/**, frontend/cli/** or skill-lib/msdmd/** change. The PostgreSQL tests still skip, because they're VM gates.

Review P3s (Review on af58b11), as normal commits with tests

Commit Finding Fix
c4ebe18 Stale collector .msdmd-XXXXXXXX temp files (from a collector killed mid-write) weren't cleaned, so they made the dirty check hold. I picked the simpler option: clean them rather than move candidates to a private dir. _clean_stale_siblings also removes regular files in the artifact's directory whose whole name matches the collector's tempfile.mkstemp(prefix=".msdmd-") pattern (.msdmd- + 8 of [a-z0-9_]) and that are older than the lease. Test: a stale one is removed and the make succeeds. A young one, a too-long name and an uppercase lookalike are kept, and they still hold.
b26fc2a A concurrent fresh status rerender (.out.fresh-status-verify) held the worker job. run_job's three _verify_runtime calls now ignore the artifact's .fresh-status-verify sibling (_status_verify_path). Test: with a status rerender file in place, the job succeeds and leaves the young file for the status run to remove.
ee7d07e worker_sandbox_run.sh parsed the unit file. It reads the unit's properties with systemctl show -p … -- $STACK_WORKER_UNIT. EnvironmentFiles= entries become EnvironmentFile=, with a - prefix when ignore_errors=yes. It exits 2 (fail closed) unless LoadState=loaded, User=$STACK_WORKER_USER (default stackorchestrator) and MemoryDenyWriteExecute=yes. Empty values are dropped, and --chdir overrides WorkingDirectory. Tests (backend/tests/test_worker_sandbox_run.py) use a stubbed systemctl and systemd-run: the exact property argv is forwarded, and a missing/wrong User, missing/no MDWE, or an unloaded unit exits 2 without running anything.
306367d Shell make-msdmd without --queue-only, and stackctl make, still registered the shell's identity. They only recovered after a failed attempt. Shell make-msdmd, fresh make, fresh run and fresh retry never observe the identity. They keep the worker-recorded identity, or worker-pending, the same way --queue-only does. A worker-pending target is only queued for the worker. Otherwise the job runs in the shell and holds, naming the differing components (no re-key), unless the shell reproduces the worker's identity. New --record-identity-here on make-msdmd/fresh make makes the shell the executor (for hosts with no worker). queue_make re-queues a terminal same-key job, so the worker can pick up a held shell attempt. Tests cover the library path (pending → queued only; mismatched env → hold with diff and no re-key; matching env → succeeds under the same key) and the CLI path (no probe while pending; --record-identity-here records).
55690e7 backend.yml pinned setup-python by tag. Pinned as actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 (a lightweight tag on that commit), like checkout. Test backend/tests/test_backend_workflow.py asserts every backend.yml action is pinned to a 40-hex SHA with a version comment.

Stack transaction (stack-update)

  • Start identity: stack d0e76d5, skill-lib 1b1a947 (after intermediate 38c6433)
  • Mutation class: pin, projection, runtime adapter
  • Affected authority / relations:
    • skill-lib operational snapshot at skill-lib/ (the default MSDMD generator root)
    • fresh-making doctrine pin
    • AHBG's skill-lib consumed pin
    • No authority transfer.
  • Files changed outside skill-lib/:
    • stack-manifest.json, STACK_MANIFEST.md: skill-lib commit is 38c6433. work_graph_sha256 was recomputed with the checker's own manifest_digest (main 889a1234… → 48ee1761…).
    • backend/fresh-making-provenance.json: doctrine.commit and local_generator_snapshot.commit are both 38c6433. The evidence compare is 891dc0d..38c6433; fresh-making/SKILL.md is unchanged and the MSDMD generator changed. The separate-refresh hmmm is resolved.
    • ahbg/integration/work-graph.json: skill-lib consumed_commit follows the stack pin (enforced by test_crossrepo_boundaries). Whether AHBG adopts the native readers remains a separate AHBG decision.
    • README.md: the licensing row says only data-visualization/SKILL.md is Apache-2.0. The provenance note now describes the exact 38c6433 snapshot.
    • AGENTS.md, backend/README.md: removed the claims that the snapshot refresh is still separate.
  • The .agents/skills/stack-update blob pin a1e9148 still equals stack-update/SKILL.md at 38c6433, so I left it unchanged. libs/* is untouched.

Backend: fresh-making on the schema-2 collector

  • Generator identity: backend/msdmd.py generator_identity no longer hashes msdmd/*.py with tree_sha256. It runs python -m msdmd.collect --print-generator-identity using the worker's own interpreter (sys.executable) and the same PYTHONPATH environment as the collector run.
    • The identity now covers every collector source: the TypeScript worker, npm manifest and lock file, schema assets and requirements.
    • It also covers the Python minor version, reader package versions, the reader module files that actually resolve, and the Node and TypeScript versions.
    • A collector that cannot report the identity fails closed (ValueError, which becomes hmmm in the ledger or a ConstraintError hold).
  • Collector refusals fail closed into the ledger. Exit 3, 4 and 5 now fail the attempt (failed). The ledger records the error with the exit code and a short code, and an operator action in hmmm.
    • 3 (reader-runtime-missing): a native reader runtime is missing.
    • 4 (schema-helper-outdated): the target's .agents/skills/msdmd/collection.ts helper is older than the schema-2 output.
    • 5 (git-visibility-unavailable): git can't list the target's visible files, or the root is git-ignored.
    • Nothing is published, and the prior accepted artifact remains the freshness authority. evaluate reports the same classification when the verifier is refused.
  • Install and provisioning:
    • New backend/ops/install_msdmd_runtime.sh runs pip install -r $STACK_SKILL_LIB_ROOT/msdmd/requirements.txt into the worker venv, then npm ci --ignore-scripts --prefix $STACK_SKILL_LIB_ROOT/msdmd, then prints --print-generator-identity --json.
    • The same steps are in the backend/README.md install block and backend/deploy/VM_SETUP.md (install block plus a runtime section).
  • Tests:
    • test_generator_identity_comes_from_the_collector_and_fails_closed
    • test_collector_refusals_fail_closed_into_the_ledger: exits 3, 4 and 5. Even a collector that leaves a file behind is not published, the accepted receipt is unchanged, and no hidden temporaries remain.

Validation (local, Node 24.15.0, Python 3.13)

  • Candidate-then-verifier reproduction through the real backend/msdmd.py: default generator root skill-lib/ at 38c6433, runtimes installed, MemoryLedger.
    • pcea at Repair language construction and qualify complete source replay #43's head c24c14b (current V1 helper): make succeeded. The candidate (.<name>.<rand>.candidate) and the verifier (.<name>.<rand>.verify) were byte-identical, and the output was published. evaluate then reran it a third time: fresh / verified.
    • Two more manual renders under different hidden names were byte-identical to the published file (sha256 3b3eefc6…). No temporaries were left over.
    • tsc --noEmit --strict --skipLibCheck --target ES2022 --module commonjs .agents/skills/msdmd/collection.ts pcea_msdmd.ts was clean.
    • ucns main 905e669 (schema-1 helper): exit 4 became a failed job with hmmm: msdmd exit 4 (schema-helper-outdated): …. Nothing was written, and evaluate reports making-fresh / no-accepted-receipt.
    • This fixes the earlier blocker, where every candidate and verifier differed. The collector now excludes its own output and the hidden candidate/verify siblings.
  • python tools/check_stack_consistency.py: pass (7 repositories, 36 research participant identities).
  • skill-lib/interdependent-work-graph/portfolio_plan.py docs/work-graphs/repository-plan-report.json: pass.
  • skill-lib/ratios/ratios_check.py on the three english-full-view-evidence files: 0 drift, 0 gaps.
  • pytest backend/tests: 17 passed, 2 skipped (PostgreSQL deployment gates), 3 subtests passed (at 131497c).
  • After the review fixes (af58b11): python -m unittest discover -s backend/tests -t .: 34 tests OK, 2 skipped, on both Python 3.13 and 3.11. tools/check_stack_consistency.py, portfolio_plan.py, the ahbg/{runtime,benchmark,integration} suites and bash -n backend/ops/*.sh all pass.
  • ahbg/runtime, ahbg/benchmark and ahbg/integration unittest suites passed. ahbg/grok a0/tests, ahbg/tests and tests passed.
  • bash -n backend/ops/*.sh: OK.
  • After the P3 fixes (55690e7): python -m unittest discover -s backend/tests -t . ran 41 tests, OK (2 PostgreSQL skips), on both Python 3.13 and 3.11. tools/check_stack_consistency.py passed (7 repositories, 36 research participant identities). portfolio_plan.py passed, and its source commit is an ancestor. The ahbg/{runtime,benchmark,integration} suites passed, and bash -n backend/ops/*.sh is OK. CI: 13/13 checks pass at 55690e7.

Deploy steps for Erin (not done here)

  1. Install the runtimes on the live VM after pulling this snapshot and before restarting the worker. Run as root, with /etc/stack-orchestrator.env sourced:
    sudo -E env STACK_VENV=/srv/stack/.venv backend/ops/install_msdmd_runtime.sh
    • It installs skill-lib/msdmd/requirements.txt into /srv/stack/.venv and runs npm ci --ignore-scripts with umask 022. Then it probes the identity in the shell and inside the worker unit's sandbox. It fails if either probe reports node or typescript as absent. Node and npm must already be installed, and node must be on the unit's default PATH.
    • VM Python and Node versions: hmmm. I validated locally with Python 3.13/3.11 and Node 24.15.0.
  2. Sandbox (MemoryDenyWriteExecute=true stays). The pin now includes skill-lib #120 (1b1a947), so Node --jitless is present. Remaining deploy acceptance: run one real systemd-run sandboxed probe on the VM (via install_msdmd_runtime.sh or worker_sandbox_run.sh) and confirm it reports node and typescript, not absent. The two PostgreSQL backend tests still skip here; they are VM gates.
  3. Re-make existing derivations. Every MSDMD derivation already registered on the VM gets a new generator identity and needs a fresh make. This is by design (fail closed).
  4. Re-sync fresh-making targets still on the V1/schema-1 helper. Every allowed target except skill-lib currently has a schema-1 .agents/skills/msdmd/collection.ts on main, so each exits 4 (a failed job, nothing published) until it is re-synced:

hmmm

  • skill-lib as a target: backend/msdmd.py runs the collector without --import-path or --snapshot-identity. The skill-lib target (helper at msdmd/collection.ts) would therefore get the default ./.agents/skills/msdmd/collection import, which only warns "not found", and its output wouldn't match skill-lib's own committed skill-lib_msdmd.ts. This is pre-existing and not changed here.
  • psfr work graph: research/psfr/WORK_GRAPH.json still has a research-local hmmm saying the snapshot is older than the doctrine it consulted. It is a historical research record, so I left it unchanged.

Ready

skill-lib #120 has merged (1b1a947). The DO-NOT-MERGE note is removed. This PR is ready for Erin's review and merge after CI is green.

Remaining deploy acceptance (not blocking the merge):

  • one real systemd-run sandboxed probe on the VM
  • the 2 PostgreSQL backend tests (VM gates; they skip in CI)

Erin merges this PR.

  • Review fixes, judgment calls:
    • Auto-supersede. A worker-pending job is superseded once, automatically. Any later generator move needs an operator to re-queue. When you run it by hand on first use, worker once has to run twice.
    • Shell status. fresh status/explain run from a shell whose environment differs report verifier-unavailable, with the differing components. Run them through worker_sandbox_run.sh for a like-for-like check.
    • Collector temp files. Stale .msdmd-XXXXXXXX files are now cleaned after the lease (c4ebe18). A collector temp file younger than the lease still holds the job, by design.
    • Shell make. A shell make against a worker-recorded identity runs the collector in the shell and holds unless the environments match. Use --queue-only, or run through worker_sandbox_run.sh. Use --record-identity-here only where no worker exists.
    • pip hashes. pip hash-checking isn't used, because skill-lib's requirements.txt has no hashes.
    • Untested on the VM. worker_sandbox_run.sh and the sandboxed probe were only dry-run here with a stub systemd-run.

Mirror The-Interdependency/skill-lib@9867ab3
into skill-lib/ exactly (git archive; blob- and mode-identical, 66 added,
83 modified, 0 removed).

Move every projection that named the old fb3b53a snapshot:
- stack-manifest.json / STACK_MANIFEST.md skill-lib pin, with
  work_graph_sha256 recomputed (889a1234... -> 3d873d1c...)
- backend/fresh-making-provenance.json: snapshot and doctrine pins at
  9867ab3 (fresh-making/SKILL.md is byte-identical to 891dc0d); the
  separate-refresh hmmm is resolved
- ahbg/integration/work-graph.json consumed_commit (test-enforced), with
  the decision/hmmm text no longer waiting on this refresh
- README licensing row: only data-visualization/SKILL.md is Apache-2.0;
  provenance note now describes the exact 9867ab3 snapshot
- README, AGENTS.md and backend/README.md: drop claims that the snapshot
  refresh is still separate

The vendored .agents/skills/stack-update blob pin (a1e9148) still equals
skill-lib@9867ab3 stack-update/SKILL.md. libs/ untouched.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T21:42:19.015453Z ae8677e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f55bda8f2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3 to +5
PyYAML==6.0.3
docstring-parser==0.18.0
tree-sitter==0.26.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install native-reader runtimes before accepting fresh output

When the documented VM setup is used, it installs only backend/requirements.txt, so these Python dependencies—and the new npm TypeScript dependency—are absent. The collector converts missing dependencies into unsupported diagnostics, while backend/msdmd.py::_run_collector does not pass --strict; consequently both generation and independent rerender can agree on the same incomplete collection and record it as fresh. Install the native-reader runtimes in the backend deployment path and/or make these diagnostics fatal for fresh-making.

AGENTS.md reference: skill-lib/AGENTS.md:L176-L179

Useful? React with 👍 / 👎.

*/
'use strict';
const fs = require('node:fs');
const ts = require('typescript');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include non-Python readers in the generator identity

For repositories containing TypeScript, collection output now depends on this worker and its locked TypeScript runtime, but backend/msdmd.py::generator_identity still calls tree_sha256 with the default (".py",) suffix. Changing typescript-reader.cjs, package-lock.json, or the schema assets therefore leaves the freshness key and verifier identity unchanged, allowing output produced by different generator code to retain the same accepted identity. Hash every implementation and lock file that can affect collection bytes.

AGENTS.md reference: AGENTS.md:L86-L89

Useful? React with 👍 / 👎.


def render_typescript(collection: dict[str, Any], *, import_path: str) -> str:
"""Render a schema-1 or schema-2 collection as a TypeScript module."""
helper = "defineMsdmdCollectionV2" if collection.get("schema_version") == SCHEMA_VERSION else "defineMsdmdCollection"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Negotiate schema 2 with each target's helper

When make-msdmd targets a checkout that still vendors the schema-1 helper—as all five current libs/*/.agents/skills/msdmd/collection.ts copies do—the new default output imports defineMsdmdCollectionV2, which those helpers do not export. The backend neither requests --legacy-blocks-only nor type-checks the artifact, so byte-identical verification can accept an unusable TypeScript module. Propagate the schema-2 helper before generation, select legacy output for old consumers, or add an explicit compatibility check.

AGENTS.md reference: skill-lib/AGENTS.md:L176-L179

Useful? React with 👍 / 👎.

Comment thread skill-lib/ratios/harmonics.py Outdated
Comment on lines +108 to +111
source_id = str(edge.get("source_id") or edge.get("from") or "")
target_id = str(edge.get("to") or "")
sources = id_files.get(source_id, set())
targets = id_files.get(target_id, set())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve resolved addresses in the semantic graph

When the semantic domain consumes an actual schema-2 collection, _reconcile_block_edges has already rewritten source_id and resolved to values to fully qualified MSDMD addresses, but this function indexes declarations only by their short id. These lookups therefore miss every resolved edge; the semantic graph has no connections and analysis either fails the three-active-node requirement or reports meaningless results. Index declaration addresses (with an explicit short-ID fallback where needed) instead of assuming legacy edge IDs.

Useful? React with 👍 / 👎.

Comment on lines +293 to +297
if peer_uid != caller.pw_uid:
raise PermissionError(
f"broker peer uid {peer_uid} is not configured caller uid {caller.pw_uid}"
)
response = execute_request(_recv_request(connection))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent shell_exec children from calling the root broker

In personal-console, shell_exec commands run as the same vmmcp UID that owns group access to the broker socket, and this UID check is the broker's only authorization. Any supposedly confined shell command can therefore connect to /run/vm-mcp/admin.sock, submit mode: "admin", and execute as root without using the visibly privileged admin_exec MCP tool or its audit/policy surface. Authenticate broker requests with a capability unavailable to shell children or isolate the broker client under a distinct service identity.

AGENTS.md reference: skill-lib/AGENTS.md:L182-L186

Useful? React with 👍 / 👎.

VM_MCP_PORT=$PORT
VM_MCP_SHELL_ENABLED=0
VM_MCP_ADMIN_SOCKET=$(systemd_quote "$ADMIN_SOCKET")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass custom broker settings to the admin service

When installation sets a non-default VM_MCP_ADMIN_SOCKET, this value is written only to /etc/vm-mcp.env, which vm-mcp.service reads; vm-mcp-admin.service does not read that file and hardcodes /run/vm-mcp/admin.sock. The MCP client consequently connects to the configured path while the broker listens on the default path, leaving user_exec and admin_exec unusable. Generate a broker drop-in or load the same environment file in the admin unit.

Useful? React with 👍 / 👎.

Comment thread skill-lib/tools/ai.sh
@@ -0,0 +1,303 @@
# ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add an interpreter shebang to the installed launcher

The new executable ai.sh starts with a ratios comment rather than a shebang. Interactive shells may fall back to interpreting an ENOEXEC file, but direct execution by execve-based callers—including subprocess.run(["ai.sh", ...])—fails with Exec format error, even though the installer exposes this file as a command on PATH. Put #!/usr/bin/env bash on the first line, as already done for install_ai.sh.

Useful? React with 👍 / 👎.

source_commit=args.source_commit,
max_file_bytes=args.max_file_bytes,
snapshot_identity=args.snapshot_identity,
generated_outputs=[args.out.resolve().relative_to(args.root.resolve()).as_posix()] if args.out and args.out.resolve().is_relative_to(args.root.resolve()) else [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep temporary output names out of collection bytes

The backend renders the candidate and verifier to distinct random paths inside the target root, and this new argument causes each path to be recorded as a generated-collection-output discovery entry. The verifier run also discovers the already-written candidate because it is not passed in generated_outputs. As a result, the two rendered files differ even for a deterministic repository, so run_job always fails its byte comparison and can never accept a fresh artifact. Normalize the recorded output to the final artifact path and exclude all temporary outputs from both scans.

AGENTS.md reference: AGENTS.md:L86-L89

Useful? React with 👍 / 👎.

Comment on lines +268 to +272
names = sorted(os.listdir(directory_fd))
except OSError as exc:
failed(parent or ".", exc, "subtree")
return
for name in names:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude ignored local files from commit-bound collections

For a clean Git checkout containing an ignored local file, this filesystem walk still reads that file and embeds its digest and parsed contents while the collection continues to claim the Git HEAD as its revision and dirty_worktree: false. Thus two machines can produce different artifacts for the same source identity, and ignored credentials such as a local private.json can be copied into a generated collection. Restrict commit-bound discovery to tracked files, or explicitly bind every additional local input through a content-snapshot identity.

AGENTS.md reference: AGENTS.md:L103-L104

Useful? React with 👍 / 👎.

…or-owned generator identity

Mirror The-Interdependency/skill-lib@38c6433
(main after skill-lib #118) into skill-lib/ exactly (git archive; blob- and
mode-identical, 3 added, 12 modified, 0 removed).

Pins moved 9867ab3 -> 38c6433:
- stack-manifest.json / STACK_MANIFEST.md, work_graph_sha256 recomputed
  (3d873d1c... -> 48ee1761...)
- backend/fresh-making-provenance.json doctrine and snapshot pins; compare
  891dc0d..38c6433 (fresh-making/SKILL.md unchanged)
- ahbg/integration/work-graph.json consumed_commit and decision text
- README provenance note

Backend:
- generator_identity delegates to `python -m msdmd.collect
  --print-generator-identity` under the worker's interpreter and collector
  environment (replaces tree_sha256); a collector that cannot report it
  fails closed.
- Collector exits 3 (reader runtime missing), 4 (target schema helper older
  than the output) and 5 (git visibility / ignored root) fail the attempt
  closed in the ledger with the exit code and an operator action; nothing
  is published and the prior accepted artifact stays authoritative.
- backend/ops/install_msdmd_runtime.sh plus README / VM_SETUP install steps:
  pip install -r $STACK_SKILL_LIB_ROOT/msdmd/requirements.txt and
  npm ci --ignore-scripts --prefix $STACK_SKILL_LIB_ROOT/msdmd.
- Tests: collector-reported identity fails closed; exit 3/4/5 refusals.
@erinepshovel-code erinepshovel-code changed the title chore(skill-lib): refresh root skill-lib snapshot to 9867ab3 chore(skill-lib): refresh root skill-lib snapshot to 38c6433 Oct 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 131497cf47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +85 to +86
def parse_yaml(data: bytes) -> Any:
import yaml # Optional reader dependency; missing installation is diagnostic.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent targets from shadowing reader dependencies

When a collected repository contains a top-level yaml.py, this import executes that repository file because backend/msdmd.py::_run_collector launches Python with the target root as cwd, which precedes the trusted generator on sys.path; the same problem applies to docstring_parser and potentially msdmd itself. This violates the syntax-only boundary and lets inspected source run with the worker's PostgreSQL identity and write access to /srv/stack-repos, while the generator identity probe runs from the generator root and does not detect the shadowing. Invoke the collector with an isolated import path or otherwise ensure dependencies resolve only from the trusted runtime.

Useful? React with 👍 / 👎.

Comment on lines +257 to +260
def count_dirty_entries(status: str) -> int:
"""Count dirty status entries while ignoring generated report files."""
if status == "hmmm":
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve an unknown Git status instead of reporting zero

When git status cannot run or its checkout is unreadable, git_value returns "hmmm", but this branch converts that unknown result to 0; the generated audit then presents the repository as clean and emits no dirty-state oddity. Preserve an explicit unknown state so a failed Git observation cannot be mistaken for evidence of a clean checkout.

AGENTS.md reference: skill-lib/AGENTS.md:L118-L119

Useful? React with 👍 / 👎.

Comment on lines +166 to 173
@mcp.tool(annotations=ADMIN_SHELL)
def admin_exec(command: str, cwd: str = "/", timeout_seconds: float = 60.0) -> dict[str, Any]:
"""Personal-console only: execute an explicitly privileged command as root."""
return _result(
lambda: policy_run_shell(
_config(), command, cwd=cwd, timeout_seconds=timeout_seconds
lambda: broker_exec(
_config(), mode="admin", user=None, command=command, cwd=cwd,
timeout_seconds=timeout_seconds,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authenticate local callers before exposing admin_exec

When VM_MCP_PROFILE=personal-console, any process belonging to any local account can connect to the unauthenticated Streamable HTTP listener on 127.0.0.1:8765 and invoke this root tool; the authenticated remote tunnel does not protect direct loopback callers. The broker's SO_PEERCRED check only sees the shared vmmcp server process, so it cannot distinguish the owner from a local attacker. Add caller authentication or expose the MCP endpoint through an OS-level transport that only the intended owner can access.

AGENTS.md reference: skill-lib/AGENTS.md:L182-L186

Useful? React with 👍 / 👎.

Comment on lines +82 to +86
if [[ ! -e "$WORK_ROOT" ]]; then
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$WORK_ROOT"
elif [[ ! -d "$WORK_ROOT" ]]; then
echo "ERROR: VM_MCP_ROOT exists but is not a directory: $WORK_ROOT" >&2
exit 5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify service access when preserving workspace ownership

When VM_MCP_ROOT already exists with restrictive ownership—for example an application checkout mode 0700 owned by its service account—this branch deliberately leaves its ownership and permissions unchanged, but the MCP service still runs as vmmcp. Installation then completes and starts a service that cannot traverse or read its configured root. Preserve ownership while granting an explicit ACL/group permission, or fail installation with an actionable error when vmmcp lacks the required access.

Useful? React with 👍 / 👎.

…ython -P

python -m puts the working directory ahead of PYTHONPATH, and the collector
and identity probe ran with cwd set to the target checkout, so a target-level
msdmd/collect.py (or yaml.py) shadowed the pinned collector; Review reproduced
a forged artifact published as fresh (stack #78 P1).

Both the executor/verifier runs and --print-generator-identity now use
cwd=generator_root and -P (Python 3.11+). Test: a target carrying its own
msdmd/ package and yaml.py never runs and its forged bytes never publish.
… and signals

- collector_failure names the status of unmapped exits (exit N) and signal
  deaths (killed by SIGKILL/SIGSEGV, with an operator hmmm) instead of
  recording only stderr.
- An OSError while spawning the executor or verifier (for example ENOMEM) is
  recorded with ledger.fail; the identity probe and git HEAD lookup turn
  OSError into fail-closed hmmm instead of raising; evaluate reports
  verifier-unavailable.
- worker.run_once records any unexpected run_job exception on the job and
  keeps looping; if the ledger itself is unreachable it still crashes so
  systemd restarts it.

Review P3s on stack #78; each with a test.
… closed

- Before checking the worktree, run_job removes hidden .<out>.<rand>.candidate,
  .verify, .<out>.<job>.accepted-backup and .<out>.fresh-status-verify files
  older than the lease (regular files only, our naming only). A stale rollback
  copy that still holds the accepted bytes while the artifact does not is
  restored instead of deleted.
- _unrelated_dirty raises ConstraintError (hold) when git status cannot run or
  exits nonzero, instead of treating the worktree as clean.

Review P3s on stack #78; each with a test.
… executes

The identity is environment-bound (interpreter, reader packages, Node,
TypeScript, sandbox). The VM_SETUP flow queued from the operator shell with
the shell's identity, the worker re-keyed it, and fresh status from the shell
re-keyed it back (stack #78 P2).

- build_spec/refresh_identities/evaluate/queue_make take observe_generator.
  stackctl --queue-only, fresh status and fresh explain never probe: they keep
  the identity the worker recorded, or mark a new derivation worker-pending.
- run_job (the executing worker) observes the identity and records its
  components (--print-generator-identity --json) in runtime. A worker-pending
  job is superseded once under the observed key; later moves still fail
  closed for the operator.
- A moved key or identity mismatch names the components that differ
  (e.g. node: v24.15.0 -> absent).
- backend/ops/worker_sandbox_run.sh runs a command via systemd-run with the
  worker unit's user, environment file and sandbox properties; VM_SETUP and
  the backend README use it for worker once / status / explain.
- New contracts stack_msdmd_target_cannot_shadow_collector and
  stack_msdmd_identity_observed_where_executed with checks.
… worker sandbox

- umask 022 so the worker user can read the installed trees; the worker user
  (STACK_WORKER_USER, default stackorchestrator) must exist.
- The identity probe (python -P, cwd at the generator root, like the backend)
  must succeed and report node and typescript, or the script exits 3.
- A second probe runs inside the worker unit's sandbox through
  worker_sandbox_run.sh (systemd-run with the unit's User, EnvironmentFile and
  MemoryDenyWriteExecute/Protect* properties); without root or systemd-run it
  exits 4 unless STACK_SANDBOX_PROBE=skip.
- pip hash-checking skipped and recorded as hmmm (no published hashes).
- VM_SETUP: MemoryDenyWriteExecute=true stays; the sandboxed probe passes only
  once the pin includes skill-lib's node --jitless change (skill-lib #120).

Review P2/P3 on stack #78; stubbed-tool tests in backend/tests/test_install_script.py.
None of stack's existing checks ran backend/tests. The new workflow runs
bash -n on backend/ops and unittest discovery on Python 3.11 (the backend's
documented minimum). PostgreSQL integration tests still skip without
STACK_TEST_DATABASE_URL and remain VM deployment gates.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af58b11c18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/msdmd.py
Comment on lines +395 to +396
_collector_command("--root", runtime["root"], "--repo", runtime["repo"],
"--out", str(output), "--source-commit", _source_sha(spec)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject sandboxed reader crashes before accepting output

When a target contains TypeScript and the collector runs in the shipped worker unit, VM_SETUP.md now documents that Node dies with SIGTRAP under MemoryDenyWriteExecute; read_typescript records that as the error diagnostic typescript_reader_failed, which is not treated as a runtime-unavailable refusal. Because this invocation still omits --strict, the candidate and verifier can both omit the TypeScript facts, compare byte-for-byte, and be accepted as fresh. This is fresh evidence beyond the earlier missing-dependency comment: the current deployment guide explicitly identifies this non-missing-runtime crash path. Pass --strict, map worker crashes to a refusal, or pin the compatible --jitless collector before accepting output.

AGENTS.md reference: AGENTS.md:L86-L89

Useful? React with 👍 / 👎.

Comment thread frontend/cli/stackctl.py
Comment on lines 98 to +100
if args.queue_only:
job, report = msdmd.queue_make(ledger, spec["target"], executor=args.executor)
job, report = msdmd.queue_make(ledger, spec["target"], executor=args.executor,
observe_generator=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Requeue terminal jobs in queue-only mode

When the current freshness key already has a terminal job—for example, a previous attempt failed, or a succeeded artifact was subsequently deleted or tampered with—enqueue deduplicates to that existing terminal row. Unlike the synchronous make path, this queue-only branch never calls ledger.retry, so it returns success with a failed/succeeded job while no queued work exists for the persistent worker to claim. Requeue terminal results before returning from --queue-only.

Useful? React with 👍 / 👎.

Comment on lines +316 to +318
def remove_path(config: VmMcpConfig, requested: str, *, recursive: bool = False) -> dict[str, Any]:
_require_workspace_write(config)
path = resolve_under_root(config.root, requested)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the final symlink in destructive path operations

When requested names a symlink whose target is also inside VM_MCP_ROOT, resolve_under_root resolves the final component before this function receives it. Consequently remove_path("link") deletes the referent rather than the symlink, and leaves a broken link behind; move_path similarly moves or replaces the referent instead of the requested directory entry. Resolve and validate the parent path while retaining the final component for lstat/unlink/os.replace operations.

Useful? React with 👍 / 👎.

WRITE_SHELL = ToolAnnotations(
readOnlyHint=False, destructiveHint=True, idempotentHint=False, openWorldHint=True
)
WRITE_FS = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark overwriting filesystem tools as destructive

When write_text targets an existing file or move_path targets an existing destination, both implementations replace existing data via os.replace, yet both tools receive destructiveHint=False through this shared annotation. MCP clients and agents may therefore omit destructive-operation safeguards for calls that can irreversibly overwrite workspace data. Use a destructive annotation for the overwrite-capable tools, or make them refuse existing destinations unless overwrite is explicitly requested.

Useful? React with 👍 / 👎.

Comment on lines +637 to +639
def _read_yaml(path: Path, data: bytes, context: dict[str, Any]) -> tuple[list, list, list]:
reader_id = "yaml-core"
value, redacted = _redact_sensitive(parse_yaml(data))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact Kubernetes Secret payload containers

When a YAML document is a Kubernetes Secret, its payload fields are conventionally data and stringData, whose child keys are arbitrary names such as tls.key or config.json. The generic key-name heuristic does not recognize those containers or many valid child names, so this reader embeds their plaintext or base64 payloads verbatim in the structured-document fact and therefore in the generated collection. Detect kind: Secret and redact the complete data and stringData mappings before emitting facts.

Useful? React with 👍 / 👎.

Comment on lines +288 to +290
tmp = path.with_name(f".{path.name}.vm-mcp.tmp")
tmp.write_bytes(encoded)
os.replace(tmp, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a unique temporary file for each write

When two stateless MCP requests write the same destination concurrently, both use the same predictable temporary pathname. One request can overwrite that temporary file before the other replaces it, causing a caller to report success even though the target contains the other request's bytes, while the losing request commonly fails because its temporary path has already been renamed. Create the temporary with a unique mkstemp/NamedTemporaryFile name in the destination directory before the atomic replace.

Useful? React with 👍 / 👎.

@erinepshovel-code
erinepshovel-code marked this pull request as draft October 6, 2026 11:29
…than the lease

The pinned collector writes --out through tempfile.mkstemp(prefix='.msdmd-')
in the target root; a collector killed mid-write left that file behind and
the dirty-worktree check held every later attempt. _clean_stale_siblings now
also removes regular files whose whole name matches \.msdmd-[a-z0-9_]{8}
once they are older than the lease. Chosen over moving candidates to a
private directory, which would change the collector's --out/root handling.

Review P3 on stack #78 (af58b11); test covers stale, young and lookalike names.
…worker job

evaluate rerenders into .<out>.fresh-status-verify; run_job's dirty checks
did not ignore it, so a status run overlapping a worker attempt held the job.
All three run_job worktree checks now ignore that sibling (the collector
already excludes *_msdmd.ts.* siblings from its inputs).

Review P3 on stack #78 (af58b11), with a test.
… relaxed hardening

The wrapper parsed the unit file by hand: it ignored drop-ins, dropped
'Key = value' lines and broke on backslash continuations, so a probe could
run without MemoryDenyWriteExecute and still pass. It now reads the loaded
unit's properties with systemctl show -p ... (drop-ins merged), converts
EnvironmentFiles entries (keeping ignore_errors as '-'), and exits 2 unless
LoadState=loaded, User=STACK_WORKER_USER (default stackorchestrator) and
MemoryDenyWriteExecute=yes. The install script passes its worker user.
VM_SETUP: install the unit and daemon-reload before the runtime install.

Review P3 on stack #78 (af58b11); stubbed systemctl/systemd-run tests.
…nding

make-msdmd without --queue-only and stackctl make still registered the shell's
generator identity (recovering only after one failed attempt). Now:

- run_job/make/retry_job take observe_generator; stackctl make-msdmd, make,
  run and retry pass False, so they keep the worker-recorded identity.
- A worker-pending target is only queued for the worker by a shell make.
- A shell run holds (naming the differing components) unless its environment
  reproduces the recorded identity; it never re-keys the derivation.
- --record-identity-here on make-msdmd / fresh make declares the shell the
  executor (dev hosts without a worker) and records its identity.
- queue_make re-queues a same-key terminal job, so a held shell attempt or a
  repair can be picked up by the worker (moved here from make()).

Review P3 on stack #78 (af58b11); tests for the library and CLI paths.
v7.0.0 is a lightweight tag on 5fda3b95a4ea91299a34e894583c3862153e4b97.
test_backend_workflow asserts every backend.yml action is SHA-pinned with a
version comment. Review P3 on stack #78 (af58b11).
Mirror The-Interdependency/skill-lib@1b1a947
(main after skill-lib #120) into skill-lib/ exactly (git archive; blob- and
mode-identical, 1 added, 7 modified, 0 removed versus 38c6433).

Pins moved 38c6433 -> 1b1a947:
- stack-manifest.json / STACK_MANIFEST.md; work_graph_sha256 recomputed
  (48ee1761... -> da8104f0...)
- backend/fresh-making-provenance.json doctrine and snapshot pins; compare
  38c6433..1b1a947 (fresh-making/SKILL.md unchanged; msdmd generator changed:
  Node --jitless and signal/probe exit 3)
- ahbg/integration/work-graph.json consumed_commit and decision text
- README provenance note; VM_SETUP.md MDWE note (pin now includes #120)
@erinepshovel-code
erinepshovel-code marked this pull request as ready for review October 6, 2026 21:17
@erinepshovel-code erinepshovel-code changed the title chore(skill-lib): refresh root skill-lib snapshot to 38c6433 chore(skill-lib): refresh root skill-lib snapshot to 1b1a947 (#120) Oct 6, 2026
Bring in weave #77 (ccf707f). Keep the skill-lib pin at 1b1a947; add weave's
ucns/uchc research participants; take main's updated weave boundary hmmm;
recompute work_graph_sha256 (6340a86d…). Drop the outdated AGENTS note that
the skill-lib snapshot refresh was still separate.
The merge kept the pre-#77 weave/stack research-participant relation, so
test_native_edges_and_forge_ownership_match_root_manifest failed looking for
"Stack-owned binary". Take main's weave participant records and recompute
work_graph_sha256 (7e2aeaee…).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae8677ec2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +132 to +138
if not timed_out and now >= deadline:
timed_out = True
_kill_group(process)
descendants_cleaned = True
if process.poll() is not None and not descendants_cleaned:
_kill_group(process)
descendants_cleaned = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Kill descendants that leave the initial process group

When a user_exec or admin_exec command daemonizes with setsid (or otherwise creates a new process group), both cleanup branches kill only process.pid's original group. The detached child survives after the shell exits or the timeout expires, so a supposedly bounded command can leave an indefinite process—including a root process for admin_exec—while the broker reports completion. Track execution in a killable cgroup or otherwise terminate descendants outside the initial process group.

Useful? React with 👍 / 👎.

Comment on lines +257 to +260
while True:
chunk = connection.recv(64 * 1024)
if not chunk:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound reads from each broker connection

When an authorized peer connects but never closes its write side—for example, after sending a partial request—this blocking recv waits forever. Because serve() handles accepted connections serially, that one stalled client prevents every later user_exec and admin_exec request until the root broker is restarted. Add a receive deadline or a length-framed request protocol so incomplete clients cannot monopolize the service.

Useful? React with 👍 / 👎.

Comment on lines +288 to +290
tmp = path.with_name(f".{path.name}.vm-mcp.tmp")
tmp.write_bytes(encoded)
os.replace(tmp, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve permissions when replacing workspace files

When write_text overwrites an existing shared workspace file under the shipped skill-lib/vm-mcp/systemd/vm-mcp.service, whose UMask is 0077, tmp.write_bytes() creates a new mode-0600 file and os.replace discards the destination's previous mode and ownership. A file previously readable by an application account or workspace group can therefore become accessible only to vmmcp after a successful MCP write. Preserve the existing metadata when replacing a file, or apply an explicit documented creation mode for new files.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant