refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Capability consent store — issue #1459 (capability trust model bypassable).
|
||||
*
|
||||
* A USER-OWNED store, living OUTSIDE any repository at `${GSD_HOME||homedir()}/.gsd/consent.json`,
|
||||
* A USER-OWNED store, living OUTSIDE any repository at `${MSD_HOME||homedir()}/.msd/consent.json`,
|
||||
* that binds each PROJECT-scope third-party capability activation to a decision the user made on
|
||||
* THIS machine. Before #1459 a project's in-repo ledger entry was treated as the consent signal —
|
||||
* but a project ledger is repo-plantable, so cloning/forging a repo activated executable surfaces
|
||||
@@ -29,9 +29,9 @@
|
||||
* (no migration/back-compat shims needed); the only version is "1".
|
||||
*
|
||||
* Exports:
|
||||
* consentStorePath(gsdHome?) — resolve the store path (GSD_HOME||homedir() rule).
|
||||
* consentStorePath(msdHome?) — resolve the store path (MSD_HOME||homedir() rule).
|
||||
* bundleContentHash(capDir) — recomputed sha512 over the whole bundle (the binding).
|
||||
* readConsentStore(gsdHome?) — bounded, NON-THROWING read; bad input → { records: {} }.
|
||||
* readConsentStore(msdHome?) — bounded, NON-THROWING read; bad input → { records: {} }.
|
||||
* hasProjectConsent({...}) — true iff a record matches the recomputed contentHash.
|
||||
* recordProjectConsent({...}) — atomic+durable+LOCKED write of a project-scope record.
|
||||
* revokeProjectConsent({...}) — atomic+LOCKED delete of a project-scope record (no-op if absent).
|
||||
@@ -78,7 +78,7 @@ const CONSENT_LOCK_MAX_ATTEMPTS = 50;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CONSENT_SCHEMA_VERSION = '1';
|
||||
const CONSENT_DIRNAME = '.gsd';
|
||||
const CONSENT_DIRNAME = '.msd';
|
||||
const CONSENT_FILE_NAME = 'consent.json';
|
||||
|
||||
/**
|
||||
@@ -212,12 +212,12 @@ function realpathProject(projectRoot: string): string {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve the consent store path. Uses the SAME `gsdHome || GSD_HOME || homedir()` rule the loader
|
||||
* Resolve the consent store path. Uses the SAME `msdHome || MSD_HOME || homedir()` rule the loader
|
||||
* and CLI use, so a consent record written by the CLI is found by the loader. The store NEVER lives
|
||||
* under a repository — it is user-owned, machine-local config.
|
||||
*/
|
||||
function consentStorePath(gsdHome?: string): string {
|
||||
const home = gsdHome || process.env['GSD_HOME'] || os.homedir();
|
||||
function consentStorePath(msdHome?: string): string {
|
||||
const home = msdHome || process.env['MSD_HOME'] || os.homedir();
|
||||
return path.join(home, CONSENT_DIRNAME, CONSENT_FILE_NAME);
|
||||
}
|
||||
|
||||
@@ -619,9 +619,9 @@ function isValidConsentRecord(rec: unknown): rec is ConsentRecord {
|
||||
* (FIFO/device), or wrong-shape store yields an empty `{ records: {} }`. Invalid individual records
|
||||
* are dropped. A store whose record count exceeds MAX_RECORDS is refused wholesale (hostile DoS).
|
||||
*/
|
||||
function readConsentStore(gsdHome?: string): ConsentStore {
|
||||
function readConsentStore(msdHome?: string): ConsentStore {
|
||||
const empty: ConsentStore = { records: {} };
|
||||
const filePath = consentStorePath(gsdHome);
|
||||
const filePath = consentStorePath(msdHome);
|
||||
let raw: string | null;
|
||||
try {
|
||||
raw = ledgerMod.readSmallRegularFile(filePath, CONSENT_MAX_BYTES);
|
||||
@@ -667,15 +667,15 @@ function readConsentStore(gsdHome?: string): ConsentStore {
|
||||
* hasOwnProperty).
|
||||
*/
|
||||
function hasProjectConsent(args: {
|
||||
gsdHome?: string;
|
||||
msdHome?: string;
|
||||
projectRoot: string;
|
||||
id: string;
|
||||
contentHash: string;
|
||||
}): boolean {
|
||||
const { gsdHome, projectRoot, id, contentHash } = args;
|
||||
const { msdHome, projectRoot, id, contentHash } = args;
|
||||
if (isUnsafeCapabilityId(id)) return false;
|
||||
if (typeof contentHash !== 'string' || !contentHash) return false;
|
||||
const store = readConsentStore(gsdHome);
|
||||
const store = readConsentStore(msdHome);
|
||||
const key = consentKey(realpathProject(projectRoot), id);
|
||||
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return false;
|
||||
const rec = store.records[key];
|
||||
@@ -689,8 +689,8 @@ function hasProjectConsent(args: {
|
||||
type ConsentLock = { path: string; token: string; dev: number | null; ino: number | null };
|
||||
|
||||
/** The consent-store lock path — keyed on the consent store DIRECTORY (one lock per machine store). */
|
||||
function consentLockPath(gsdHome?: string): string {
|
||||
return path.join(path.dirname(consentStorePath(gsdHome)), '.consent.lock');
|
||||
function consentLockPath(msdHome?: string): string {
|
||||
return path.join(path.dirname(consentStorePath(msdHome)), '.consent.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -769,8 +769,8 @@ function renameBackoff(): void {
|
||||
* The on-disk JSON uses the unambiguous JSON-object disk key (WIN-3); the in-memory store is keyed by
|
||||
* the canonical NUL key, so we re-key here.
|
||||
*/
|
||||
function writeConsentStore(gsdHome: string | undefined, store: ConsentStore): void {
|
||||
const filePath = consentStorePath(gsdHome);
|
||||
function writeConsentStore(msdHome: string | undefined, store: ConsentStore): void {
|
||||
const filePath = consentStorePath(msdHome);
|
||||
const dir = path.dirname(filePath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
@@ -848,7 +848,7 @@ function writeConsentStore(gsdHome: string | undefined, store: ConsentStore): vo
|
||||
* written. (The OLD code returned a null handle and proceeded unlocked — that is the bug.)
|
||||
*/
|
||||
function recordProjectConsent(args: {
|
||||
gsdHome?: string;
|
||||
msdHome?: string;
|
||||
projectRoot: string;
|
||||
id: string;
|
||||
integrity: string;
|
||||
@@ -856,7 +856,7 @@ function recordProjectConsent(args: {
|
||||
contentHash: string;
|
||||
reviewerHost?: string;
|
||||
}): void {
|
||||
const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
|
||||
const { msdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
|
||||
if (isUnsafeCapabilityId(id)) {
|
||||
throw new Error(
|
||||
`Invalid capability id "${String(id)}": must match /^[a-z][a-z0-9-]*$/ (kebab-case, lowercase). ` +
|
||||
@@ -870,27 +870,27 @@ function recordProjectConsent(args: {
|
||||
);
|
||||
}
|
||||
const realRoot = realpathProject(projectRoot);
|
||||
const lockDir = path.dirname(consentStorePath(gsdHome));
|
||||
const lockDir = path.dirname(consentStorePath(msdHome));
|
||||
try { fs.mkdirSync(lockDir, { recursive: true }); } catch { /* best-effort — write also mkdirs */ }
|
||||
// #1459 finding 3: never proceed UNLOCKED. A null handle (live holder / contention budget exhausted)
|
||||
// → throw rather than risk a lost update.
|
||||
const lock = acquireConsentLock(lockDir);
|
||||
if (lock === null) {
|
||||
throw new Error(
|
||||
`recordProjectConsent: could not acquire the consent-store lock at ${consentLockPath(gsdHome)} ` +
|
||||
`recordProjectConsent: could not acquire the consent-store lock at ${consentLockPath(msdHome)} ` +
|
||||
`(another writer holds it). Refusing to write the consent store UNLOCKED (a lost-update risk). ` +
|
||||
`Retry; if a stale lock persists past the deadman it is reclaimed automatically.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const store = readConsentStore(gsdHome);
|
||||
const store = readConsentStore(msdHome);
|
||||
const key = consentKey(realRoot, id);
|
||||
// CONSENT-MAXRECORDS-WRITE-1: enforce the cap BEFORE the write. A re-record of an EXISTING key
|
||||
// does not grow the store (allowed); only ADDING a new key when already at the cap is refused.
|
||||
if (!Object.prototype.hasOwnProperty.call(store.records, key) && Object.keys(store.records).length >= MAX_RECORDS) {
|
||||
throw new Error(
|
||||
`consent store full: already at the maximum of ${MAX_RECORDS} consent records. Revoke an ` +
|
||||
`unused consent (gsd capability trust revoke) before recording a new one.`,
|
||||
`unused consent (msd capability trust revoke) before recording a new one.`,
|
||||
);
|
||||
}
|
||||
store.records[key] = {
|
||||
@@ -905,7 +905,7 @@ function recordProjectConsent(args: {
|
||||
// or a non-lane capability keeps a byte-identical record shape.
|
||||
...(typeof reviewerHost === 'string' && reviewerHost ? { reviewerHost } : {}),
|
||||
};
|
||||
writeConsentStore(gsdHome, store);
|
||||
writeConsentStore(msdHome, store);
|
||||
} finally {
|
||||
releaseConsentLock(lock);
|
||||
}
|
||||
@@ -920,27 +920,27 @@ function recordProjectConsent(args: {
|
||||
* doing an unlocked read-modify-write (the lost-update vector). An ABSENT-record no-op still happens
|
||||
* UNDER the lock (so a concurrent record cannot interleave); only a genuine lock-acquire failure throws.
|
||||
*/
|
||||
function revokeProjectConsent(args: { gsdHome?: string; projectRoot: string; id: string }): void {
|
||||
const { gsdHome, projectRoot, id } = args;
|
||||
function revokeProjectConsent(args: { msdHome?: string; projectRoot: string; id: string }): void {
|
||||
const { msdHome, projectRoot, id } = args;
|
||||
if (isUnsafeCapabilityId(id)) return; // an unsafe id was never stored — nothing to revoke.
|
||||
const realRoot = realpathProject(projectRoot);
|
||||
const lockDir = path.dirname(consentStorePath(gsdHome));
|
||||
const lockDir = path.dirname(consentStorePath(msdHome));
|
||||
try { fs.mkdirSync(lockDir, { recursive: true }); } catch { /* best-effort — write also mkdirs */ }
|
||||
// #1459 finding 3: never proceed UNLOCKED — a null handle throws rather than deleting unlocked.
|
||||
const lock = acquireConsentLock(lockDir);
|
||||
if (lock === null) {
|
||||
throw new Error(
|
||||
`revokeProjectConsent: could not acquire the consent-store lock at ${consentLockPath(gsdHome)} ` +
|
||||
`revokeProjectConsent: could not acquire the consent-store lock at ${consentLockPath(msdHome)} ` +
|
||||
`(another writer holds it). Refusing to modify the consent store UNLOCKED (a lost-update risk). ` +
|
||||
`Retry; if a stale lock persists past the deadman it is reclaimed automatically.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
const store = readConsentStore(gsdHome);
|
||||
const store = readConsentStore(msdHome);
|
||||
const key = consentKey(realRoot, id);
|
||||
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return; // absent — no-op.
|
||||
delete store.records[key];
|
||||
writeConsentStore(gsdHome, store);
|
||||
writeConsentStore(msdHome, store);
|
||||
} finally {
|
||||
releaseConsentLock(lock);
|
||||
}
|
||||
@@ -962,14 +962,14 @@ function revokeProjectConsent(args: { gsdHome?: string; projectRoot: string; id:
|
||||
* Non-throwing: a missing, corrupt, or oversized store yields `undefined` like any other absence.
|
||||
*/
|
||||
function readConsentedReviewerHost(args: {
|
||||
gsdHome?: string;
|
||||
msdHome?: string;
|
||||
projectRoot: string;
|
||||
id: string;
|
||||
}): string | undefined {
|
||||
const { gsdHome, projectRoot, id } = args;
|
||||
const { msdHome, projectRoot, id } = args;
|
||||
if (isUnsafeCapabilityId(id)) return undefined;
|
||||
try {
|
||||
const store = readConsentStore(gsdHome);
|
||||
const store = readConsentStore(msdHome);
|
||||
const key = consentKey(realpathProject(projectRoot), id);
|
||||
if (!Object.prototype.hasOwnProperty.call(store.records, key)) return undefined;
|
||||
const host = store.records[key].reviewerHost;
|
||||
|
||||
Reference in New Issue
Block a user