@jstn-sdk/ma 0.1.13 → 0.14.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/.agents/plugins/marketplace.json +0 -0
- package/.codex/agents/Architect.toml +1 -0
- package/.codex/agents/Auditor.toml +1 -0
- package/.codex/agents/Builder.toml +1 -0
- package/.codex/agents/Flow.toml +1 -0
- package/.codex/agents/Sage.toml +1 -0
- package/.codex/agents/Vibe.toml +1 -0
- package/.codex/hooks.json +12 -2
- package/.codex/prompts/enforcement.md +0 -0
- package/.codex/prompts/onboarding.md +6 -1
- package/.codex/prompts/release-rules.md +0 -0
- package/.codex/prompts/skill-contract.md +0 -0
- package/COVERAGE.md +7 -7
- package/DEMO.md +15 -6
- package/LICENSE +0 -0
- package/README.md +126 -793
- package/bin/ma.js +585 -97
- package/data/clone-data.ledger.json +0 -0
- package/data/clone-data.proof.json +0 -0
- package/data/clone-data.rvf +0 -0
- package/docs/README.md +8 -2
- package/docs/autonomous-tasks.md +46 -0
- package/docs/codex-integration.md +16 -0
- package/docs/getting-started.md +28 -4
- package/docs/installed-sdk.md +0 -0
- package/docs/mcp-setup.md +30 -1
- package/docs/qa/{release-issue-gates-0.1.13.json → release-issue-gates-0.14.0.json} +77 -77
- package/docs/qa/release-readiness-0.1.5.md +0 -0
- package/docs/qa/{release-readiness-0.1.13.md → release-readiness-0.14.0.md} +12 -12
- package/docs/quality.md +24 -0
- package/docs/reference/native-engineering-patterns.md +0 -0
- package/docs/reference/native-security-playbooks.md +0 -0
- package/docs/reference/native-source-selection.md +0 -0
- package/docs/reference/native-style-and-deslop.md +0 -0
- package/docs/release-spec.md +27 -14
- package/docs/skills.md +1 -1
- package/index.js +73 -0
- package/mcp/collections.json +0 -0
- package/mcp/fallback.json +0 -0
- package/mcp/local/code-intel.js +179 -41
- package/mcp/local/context.js +180 -0
- package/mcp/local/memory.js +17 -6
- package/mcp/local/playbooks.js +0 -0
- package/mcp/local/quality.js +39 -0
- package/mcp/local/state.js +7 -3
- package/mcp/local/team-run.js +8 -3
- package/mcp/local/trace.js +0 -0
- package/mcp/local-capabilities.json +9 -0
- package/mcp/native-playbooks.json +0 -0
- package/mcp/servers.json +0 -0
- package/package.json +26 -4
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +2 -2
- package/plugins/meta-architect/obsidian/main.js +137 -4
- package/plugins/meta-architect/obsidian/manifest.json +1 -1
- package/plugins/meta-architect/obsidian/styles.css +0 -0
- package/plugins/meta-architect/skills/align/SKILL.md +0 -0
- package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +0 -0
- package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/build/SKILL.md +0 -0
- package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
- package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
- package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
- package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +0 -0
- package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/SKILL.md +0 -0
- package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +0 -0
- package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +0 -0
- package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/sage/references/source-selection.md +0 -0
- package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
- package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/references/security-playbooks.md +0 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
- package/schemas/autonomous-task-queue.schema.json +56 -0
- package/schemas/handoff-packet.schema.json +36 -0
- package/schemas/setup-receipt.schema.json +28 -0
- package/schemas/skill-frontmatter.schema.json +20 -0
- package/schemas/support-bundle.schema.json +37 -0
- package/schemas/task-contract.schema.json +31 -0
- package/scripts/active-autonomy-hook.mjs +141 -11
- package/scripts/biome-staged.js +0 -0
- package/scripts/build-linux-packages.mjs +7 -3
- package/scripts/cleanup-test-fixtures.sh +55 -0
- package/scripts/context-hydration-hook.mjs +89 -0
- package/scripts/demo-smoke.js +285 -0
- package/scripts/doctor.js +10 -8
- package/scripts/install.sh +33 -4
- package/scripts/install.sh.sha256 +1 -0
- package/scripts/linux-package-lib.mjs +0 -0
- package/scripts/linux-package-smoke.mjs +2 -2
- package/scripts/package-size-check.mjs +26 -0
- package/scripts/plugin-sync.js +83 -17
- package/scripts/postinstall.js +17 -10
- package/scripts/prepack.js +9 -0
- package/scripts/ralph/prompt.md +0 -0
- package/scripts/release-metadata.js +0 -0
- package/scripts/release-sync.js +72 -7
- package/scripts/release-verify.js +100 -7
- package/scripts/setup-npmrc.js +48 -23
- package/scripts/skills-install.js +9 -2
- package/scripts/skills-manifest.js +2 -21
- package/scripts/skills-pack.js +2 -2
- package/scripts/skills-validate.js +5 -55
- package/skills/align/SKILL.md +0 -0
- package/skills/align/agents/openai.yaml +0 -0
- package/skills/align/references/shared-language.md +0 -0
- package/skills/arch/SKILL.md +0 -0
- package/skills/arch/agents/openai.yaml +0 -0
- package/skills/build/SKILL.md +0 -0
- package/skills/build/agents/openai.yaml +0 -0
- package/skills/cleanup/SKILL.md +0 -0
- package/skills/cleanup/agents/openai.yaml +0 -0
- package/skills/cleanup/references/style-and-deslop.md +0 -0
- package/skills/diagnose/SKILL.md +0 -0
- package/skills/diagnose/agents/openai.yaml +0 -0
- package/skills/flow/SKILL.md +0 -0
- package/skills/flow/agents/openai.yaml +0 -0
- package/skills/index.json +0 -0
- package/skills/maestro/SKILL.md +0 -0
- package/skills/maestro/agents/openai.yaml +0 -0
- package/skills/maestro/references/core-release-rules.md +0 -0
- package/skills/maestro/references/native-ingest-map.md +0 -0
- package/skills/sage/SKILL.md +0 -0
- package/skills/sage/agents/openai.yaml +0 -0
- package/skills/sage/references/source-selection.md +0 -0
- package/skills/tdd/SKILL.md +0 -0
- package/skills/tdd/agents/openai.yaml +0 -0
- package/skills/vet/SKILL.md +0 -0
- package/skills/vet/agents/openai.yaml +0 -0
- package/skills/vet/references/security-playbooks.md +0 -0
- package/skills/vibe/SKILL.md +0 -0
- package/skills/vibe/agents/openai.yaml +0 -0
- package/sprint/00-idea.md +0 -0
- package/sprint/01-architecture.md +0 -0
- package/sprint/02-oss-evidence.md +0 -0
- package/sprint/03-logic.md +0 -0
- package/sprint/04-security.md +0 -0
- package/sprint/05-dx-ux.md +0 -0
- package/sprint/06-build-plan.md +0 -0
- package/sprint/07-release.md +0 -0
- package/src/agents.js +279 -0
- package/src/bootstrap.js +252 -63
- package/src/build-gate.js +0 -0
- package/src/codex-app-server.js +291 -0
- package/src/decision-log.js +0 -0
- package/src/fs-utils.js +175 -4
- package/src/launcher.js +24 -21
- package/src/mcp-config.js +9 -1
- package/src/mcp-live-client.js +140 -7
- package/src/paths.js +6 -1
- package/src/policy.js +2 -2
- package/src/prelaunch.js +189 -0
- package/src/quality/ai-quality-orchestrator.js +328 -0
- package/src/release-issue-gates.js +62 -0
- package/src/release-operations.js +62 -0
- package/src/release-state.js +0 -0
- package/src/runtime/active-autonomy-core.js +0 -0
- package/src/runtime/agent-compat.js +31 -0
- package/src/runtime/alignment-sentinel.js +0 -0
- package/src/runtime/architect-review.js +248 -10
- package/src/runtime/autonomous-tasks.js +408 -0
- package/src/runtime/build-readiness.js +0 -0
- package/src/runtime/code-graph-rehearse.js +0 -0
- package/src/runtime/codeburn-core.js +112 -0
- package/src/runtime/context-authority.js +40 -0
- package/src/runtime/context-economy-core.js +40 -0
- package/src/runtime/continuity-graph.js +201 -0
- package/src/runtime/continuity-notes.js +71 -26
- package/src/runtime/core-source-ingest.js +0 -0
- package/src/runtime/detached-provider.js +82 -14
- package/src/runtime/doctor-report.js +18 -0
- package/src/runtime/environment-awareness-core.js +0 -0
- package/src/runtime/exposure-catalog.js +0 -0
- package/src/runtime/graphify-core.js +167 -0
- package/src/runtime/guidance-stack.js +9 -2
- package/src/runtime/handoff-packets.js +92 -0
- package/src/runtime/headroom-core.js +104 -0
- package/src/runtime/helper-orchestration-core.js +0 -0
- package/src/runtime/hook-profiles.js +20 -0
- package/src/runtime/learning-loop-core.js +110 -4
- package/src/runtime/live-agent-verification.js +122 -0
- package/src/runtime/maestro-events.js +0 -0
- package/src/runtime/maestro-manager.js +0 -0
- package/src/runtime/maestro-output.js +58 -0
- package/src/runtime/maestro-state.js +0 -0
- package/src/runtime/managed-markdown.js +29 -0
- package/src/runtime/mcp-authority.js +147 -0
- package/src/runtime/mcp-policy.js +0 -0
- package/src/runtime/obsidian-integration-core.js +255 -17
- package/src/runtime/obsidian-plugin-bridge.js +344 -38
- package/src/runtime/orchestrator.js +0 -0
- package/src/runtime/pi-maestro-core.js +89 -0
- package/src/runtime/preferences.js +79 -0
- package/src/runtime/project-context.js +636 -0
- package/src/runtime/prompt-strategy-core.js +0 -0
- package/src/runtime/quorum-review.js +0 -0
- package/src/runtime/ralph-execution-core.js +0 -0
- package/src/runtime/redaction-gateway.js +218 -19
- package/src/runtime/runtime-state.js +94 -2
- package/src/runtime/schema-migrations.js +162 -0
- package/src/runtime/semantic-recording-core.js +0 -0
- package/src/runtime/signal-hooks.js +13 -0
- package/src/runtime/skills-registry-export.js +92 -8
- package/src/runtime/startup-path.js +0 -0
- package/src/runtime/task-contracts.js +83 -0
- package/src/runtime/universal-plugin-broker-core.js +373 -50
- package/src/runtime/workspace-intelligence-runtime.js +0 -0
- package/src/runtime/workspace-virtualizer.js +0 -0
- package/src/runtime/workspaces.js +0 -0
- package/src/runtime-artifacts.js +0 -0
- package/src/setup-lifecycle.js +359 -0
- package/src/skill-frontmatter.js +78 -0
- package/src/skill-installer.js +226 -71
- package/src/skills.js +297 -19
- package/src/state-sync.js +0 -0
- package/src/test-fixtures.js +543 -0
- package/src/tui/grid.js +67 -0
- package/src/tui/status-grid.js +23 -0
- package/support-bundle.json +114 -0
- package/templates/AGENTS.md +6 -2
- package/templates/catalog-manifest.json +0 -0
- package/templates/model-instructions/core.md +0 -0
- package/templates/model-instructions/release.md +0 -0
- package/templates/model-instructions/security.md +0 -0
- package/templates/quality/ai-quality-rules.yml +37 -0
- package/docs/assets/image/Screenshot(1).png +0 -0
- package/docs/assets/image/Screenshot(2).png +0 -0
- package/docs/assets/image/Screenshot(3).png +0 -0
- package/docs/assets/image/Screenshot(4).png +0 -0
- package/docs/assets/image/Screenshot(5).png +0 -0
- package/docs/assets/image/Screenshot(6).png +0 -0
- package/docs/assets/image/Screenshot(7).png +0 -0
- package/docs/assets/image/Screenshot(8).png +0 -0
- package/docs/assets/image/Screenshot(9).png +0 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +0 -8
- package/docs/onboarding.md +0 -81
- package/docs/prompt-guidance-contract.md +0 -17
- package/docs/prompt-guidance-fragments/active-autonomy-core.md +0 -7
- package/docs/skills-publishing.md +0 -255
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function createMaestroView(maestroState) {
|
|
2
|
+
const tracks = Object.entries(maestroState?.runtime_tracks ?? {}).map(([id, track]) => ({
|
|
3
|
+
id,
|
|
4
|
+
gate: track.active_gate ?? "none",
|
|
5
|
+
status: track.status ?? "unknown",
|
|
6
|
+
blockers: Array.isArray(track.blockers) ? track.blockers : [],
|
|
7
|
+
}));
|
|
8
|
+
const locks = Object.entries(maestroState?.downstream_lock_table ?? {}).map(([gate, lock]) => ({
|
|
9
|
+
gate,
|
|
10
|
+
status: lock.is_locked ? "locked" : "unlocked",
|
|
11
|
+
lockedBy: Array.isArray(lock.locked_by) ? lock.locked_by : [],
|
|
12
|
+
unlockCriteria: lock.unlock_criteria ?? null,
|
|
13
|
+
}));
|
|
14
|
+
const blockers = tracks.flatMap((track) =>
|
|
15
|
+
track.blockers.map((message) => `${track.id}: ${message}`),
|
|
16
|
+
);
|
|
17
|
+
return {
|
|
18
|
+
schemaVersion: "0.1.0",
|
|
19
|
+
scope: "maestro",
|
|
20
|
+
globalStatus: maestroState?.global_status ?? "unknown",
|
|
21
|
+
orchestrationId: maestroState?.orchestration_id ?? null,
|
|
22
|
+
tracks,
|
|
23
|
+
locks,
|
|
24
|
+
blockers,
|
|
25
|
+
nextAction:
|
|
26
|
+
blockers[0] ??
|
|
27
|
+
(locks.some((lock) => lock.status === "locked")
|
|
28
|
+
? "Resolve downstream locks"
|
|
29
|
+
: "Continue active lane"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatMaestroView(view) {
|
|
34
|
+
const lines = [
|
|
35
|
+
"Maestro View",
|
|
36
|
+
"Meta-Architect / Maestro",
|
|
37
|
+
"========================",
|
|
38
|
+
`Global status: ${view.globalStatus}`,
|
|
39
|
+
`Orchestration id: ${view.orchestrationId ?? "not started"}`,
|
|
40
|
+
`Status: ${view.globalStatus}`,
|
|
41
|
+
`Run: ${view.orchestrationId ?? "not started"}`,
|
|
42
|
+
"",
|
|
43
|
+
"Lane Gate Status",
|
|
44
|
+
"---------- -------------------- ----------",
|
|
45
|
+
];
|
|
46
|
+
if (view.tracks.length === 0) lines.push("none");
|
|
47
|
+
for (const track of view.tracks)
|
|
48
|
+
lines.push(`${track.id.padEnd(10)} ${track.gate.padEnd(20)} ${track.status}`);
|
|
49
|
+
lines.push("", "Blockers");
|
|
50
|
+
lines.push(...(view.blockers.length ? view.blockers.map((item) => `- ${item}`) : ["- none"]));
|
|
51
|
+
lines.push("", `Next: ${view.nextAction}`, "", "Downstream locks");
|
|
52
|
+
lines.push(
|
|
53
|
+
...(view.locks.length
|
|
54
|
+
? view.locks.map((lock) => `- ${lock.gate}: ${lock.status}`)
|
|
55
|
+
: ["- none"]),
|
|
56
|
+
);
|
|
57
|
+
return `${lines.join("\n")}\n`;
|
|
58
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const START = "<!-- MA:MANAGED:START";
|
|
2
|
+
const END = "<!-- MA:MANAGED:END -->";
|
|
3
|
+
|
|
4
|
+
export function createManagedMarkdownBlock({ id, source, body }) {
|
|
5
|
+
return `${START} id=${id} source=${source} -->\n${body.trim()}\n${END}\n`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function replaceManagedMarkdownBlock(existing, generated, id) {
|
|
9
|
+
const marker = new RegExp(
|
|
10
|
+
`<!-- MA:MANAGED:START id=${escapeRegExp(id)}[^>]*-->[\\s\\S]*?${END}`,
|
|
11
|
+
"g",
|
|
12
|
+
);
|
|
13
|
+
const starts = existing.match(/<!-- MA:MANAGED:START\b/g)?.length ?? 0;
|
|
14
|
+
const ends = existing.match(new RegExp(escapeRegExp(END), "g"))?.length ?? 0;
|
|
15
|
+
if (starts === 1 && ends === 1 && marker.test(existing))
|
|
16
|
+
return existing.replace(marker, generated.trim());
|
|
17
|
+
return `${existing.trimEnd()}\n\n${generated}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function removeManagedMarkdownBlock(existing, id) {
|
|
21
|
+
const marker = new RegExp(
|
|
22
|
+
`\\n?<!-- MA:MANAGED:START id=${escapeRegExp(id)}[^>]*-->[\\s\\S]*?${END}\\n?`,
|
|
23
|
+
);
|
|
24
|
+
return existing.replace(marker, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function escapeRegExp(value) {
|
|
28
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJson } from "../fs-utils.js";
|
|
5
|
+
import { getRuntimeWritePath } from "../paths.js";
|
|
6
|
+
import { resolveHookProfile } from "./hook-profiles.js";
|
|
7
|
+
import { maskSensitiveText } from "./redaction-gateway.js";
|
|
8
|
+
import { queueMailboxProposal } from "./runtime-state.js";
|
|
9
|
+
|
|
10
|
+
const writePolicies = {
|
|
11
|
+
"state.sync_release_status": "state-write",
|
|
12
|
+
"state.append_decision": "state-write",
|
|
13
|
+
"memory.store_note": "memory-write",
|
|
14
|
+
"team_run.submit_task": "task-control",
|
|
15
|
+
"team_run.control_task": "task-control",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function getField(options, camel, snake) {
|
|
19
|
+
return options?.[camel] ?? options?.[snake];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getProfile(options) {
|
|
23
|
+
const requested = getField(options, "profile", "profile") ?? process.env.MA_MCP_PROFILE;
|
|
24
|
+
return requested ? resolveHookProfile(requested) : resolveHookProfile();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateMetadata(options) {
|
|
28
|
+
const metadata = {
|
|
29
|
+
actor: getField(options, "actor", "actor"),
|
|
30
|
+
authority: options?.authority ?? getField(options, "authorityBoundary", "authority_boundary"),
|
|
31
|
+
reason: getField(options, "reason", "reason"),
|
|
32
|
+
evidence: getField(options, "evidence", "evidence"),
|
|
33
|
+
};
|
|
34
|
+
const missing = Object.entries(metadata)
|
|
35
|
+
.filter(([, value]) => value == null || (typeof value === "string" && value.trim() === ""))
|
|
36
|
+
.map(([key]) => key);
|
|
37
|
+
return { metadata, missing };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function receiptPath() {
|
|
41
|
+
return path.join(getRuntimeWritePath("mcp", "receipts"), `${Date.now()}-${randomUUID()}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function writeAuditReceipt({ tool, classification, profile, metadata, outcome, reason }) {
|
|
45
|
+
const safe = maskSensitiveText(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
actor: metadata.actor,
|
|
48
|
+
authority: metadata.authority,
|
|
49
|
+
reason: metadata.reason,
|
|
50
|
+
evidence: metadata.evidence,
|
|
51
|
+
}),
|
|
52
|
+
).sanitizedText;
|
|
53
|
+
const receipt = {
|
|
54
|
+
schemaVersion: "0.1.0",
|
|
55
|
+
record_type: "mcp_write_audit_receipt",
|
|
56
|
+
tool,
|
|
57
|
+
classification,
|
|
58
|
+
profile: profile.id,
|
|
59
|
+
outcome,
|
|
60
|
+
denial_reason: reason ?? null,
|
|
61
|
+
metadata: JSON.parse(safe),
|
|
62
|
+
recordedAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
const target = receiptPath();
|
|
65
|
+
await writeJson(target, receipt);
|
|
66
|
+
await fs.chmod(target, 0o600).catch(() => {});
|
|
67
|
+
return target;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function listMcpWritePolicies() {
|
|
71
|
+
return Object.entries(writePolicies).map(([tool, classification]) => ({ tool, classification }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function mcpWriteCapabilityStatus() {
|
|
75
|
+
const profile = resolveHookProfile(
|
|
76
|
+
process.env.MA_MCP_PROFILE ?? process.env.MA_PROFILE ?? "local",
|
|
77
|
+
);
|
|
78
|
+
return {
|
|
79
|
+
profile: profile.id,
|
|
80
|
+
readOnly: profile.readOnly === true,
|
|
81
|
+
tools: listMcpWritePolicies(),
|
|
82
|
+
detail: profile.readOnly
|
|
83
|
+
? `MCP writes disabled by ${profile.id} profile`
|
|
84
|
+
: `MCP writes require actor, authority, reason, and evidence in ${profile.id} profile`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function loadMcpAuditReceipts() {
|
|
89
|
+
const root = getRuntimeWritePath("mcp", "receipts");
|
|
90
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
91
|
+
const receipts = [];
|
|
92
|
+
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json"))) {
|
|
93
|
+
try {
|
|
94
|
+
receipts.push(JSON.parse(await fs.readFile(path.join(root, entry.name), "utf8")));
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore incomplete receipts; doctor can still report the directory.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return receipts.sort((a, b) => `${a.recordedAt}`.localeCompare(`${b.recordedAt}`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function withMcpWriteGate({ tool, options = {}, payload = {} }, operation) {
|
|
103
|
+
const classification = writePolicies[tool];
|
|
104
|
+
if (!classification) throw new Error(`Unknown MCP write tool: ${tool}`);
|
|
105
|
+
const profile = getProfile(options);
|
|
106
|
+
const { metadata, missing } = validateMetadata(options);
|
|
107
|
+
|
|
108
|
+
if (profile.readOnly === true) {
|
|
109
|
+
const auditReceipt = await writeAuditReceipt({
|
|
110
|
+
tool,
|
|
111
|
+
classification,
|
|
112
|
+
profile,
|
|
113
|
+
metadata,
|
|
114
|
+
outcome: "denied",
|
|
115
|
+
reason: "profile_read_only",
|
|
116
|
+
});
|
|
117
|
+
return { proposed: false, denied: true, reason: "profile_read_only", auditReceipt };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
const proposalPath = await queueMailboxProposal({
|
|
122
|
+
actor: metadata.actor ?? "mcp-unknown",
|
|
123
|
+
kind: `mcp-${classification}`,
|
|
124
|
+
payload: { tool, payload, missingAuthority: missing },
|
|
125
|
+
});
|
|
126
|
+
const auditReceipt = await writeAuditReceipt({
|
|
127
|
+
tool,
|
|
128
|
+
classification,
|
|
129
|
+
profile,
|
|
130
|
+
metadata,
|
|
131
|
+
outcome: "proposed",
|
|
132
|
+
reason: `missing_authority:${missing.join(",")}`,
|
|
133
|
+
});
|
|
134
|
+
return { proposed: true, proposalPath, auditReceipt, reason: "missing_authority" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = await operation(metadata);
|
|
138
|
+
const auditReceipt = await writeAuditReceipt({
|
|
139
|
+
tool,
|
|
140
|
+
classification,
|
|
141
|
+
profile,
|
|
142
|
+
metadata,
|
|
143
|
+
outcome: result?.proposed ? "proposed" : "allowed",
|
|
144
|
+
reason: result?.proposed ? "leader_authority_required" : null,
|
|
145
|
+
});
|
|
146
|
+
return { ...result, auditReceipt };
|
|
147
|
+
}
|
|
File without changes
|
|
@@ -3,12 +3,50 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
5
5
|
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
6
|
+
import { createManagedMarkdownBlock } from "./managed-markdown.js";
|
|
6
7
|
import { createDefaultObsidianPluginBridgeManifest } from "./obsidian-plugin-bridge.js";
|
|
7
8
|
|
|
8
9
|
export const obsidianIntegrationSchemaVersion = "0.1.0";
|
|
9
10
|
export const obsidianGraphMapNotePath = "Meta-Architect/Map of Content.md";
|
|
11
|
+
const obsidianConfigPath = () => getRuntimeSubsystemPath("obsidian", "config.json");
|
|
10
12
|
const obsidianGraphSectionHeading = "## Obsidian Graph Links";
|
|
11
13
|
|
|
14
|
+
export async function configureObsidianVault(vaultPath) {
|
|
15
|
+
const resolved = path.resolve(vaultPath ?? "");
|
|
16
|
+
const stat = await fs.stat(resolved).catch(() => null);
|
|
17
|
+
if (!stat?.isDirectory()) throw new Error(`Invalid Obsidian vault: ${resolved}`);
|
|
18
|
+
const config = {
|
|
19
|
+
schemaVersion: obsidianIntegrationSchemaVersion,
|
|
20
|
+
vaultPath: resolved,
|
|
21
|
+
updatedAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
await writeJson(obsidianConfigPath(), config);
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadObsidianVaultConfig() {
|
|
28
|
+
return readJson(obsidianConfigPath()).catch(() => null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resolveObsidianVault(vaultPath) {
|
|
32
|
+
const configured =
|
|
33
|
+
vaultPath || process.env.MA_OBSIDIAN_VAULT || (await loadObsidianVaultConfig())?.vaultPath;
|
|
34
|
+
if (!configured)
|
|
35
|
+
throw new Error("Obsidian vault path required; run `ma obsidian configure <vault-path>`");
|
|
36
|
+
const resolved = path.resolve(configured);
|
|
37
|
+
const stat = await fs.stat(resolved).catch(() => null);
|
|
38
|
+
if (!stat?.isDirectory()) throw new Error(`Invalid Obsidian vault: ${resolved}`);
|
|
39
|
+
return fs.realpath(resolved);
|
|
40
|
+
}
|
|
41
|
+
const allowedObsidianPluginActions = new Set([
|
|
42
|
+
"capture_active_note",
|
|
43
|
+
"create_note",
|
|
44
|
+
"index_vault",
|
|
45
|
+
"rename_note",
|
|
46
|
+
"select-notes",
|
|
47
|
+
"write_attachment",
|
|
48
|
+
]);
|
|
49
|
+
|
|
12
50
|
export function getObsidianBridgePath() {
|
|
13
51
|
return getRuntimeSubsystemPath("context", "obsidian-bridge.json");
|
|
14
52
|
}
|
|
@@ -104,6 +142,9 @@ export function createObsidianPluginRequest({
|
|
|
104
142
|
if (!action || typeof action !== "string") {
|
|
105
143
|
throw new Error("Obsidian plugin request requires an action");
|
|
106
144
|
}
|
|
145
|
+
if (!allowedObsidianPluginActions.has(action)) {
|
|
146
|
+
throw new Error(`Obsidian plugin request action is not allowed: ${action}`);
|
|
147
|
+
}
|
|
107
148
|
|
|
108
149
|
return {
|
|
109
150
|
record_type: "obsidian_plugin_request",
|
|
@@ -111,8 +152,8 @@ export function createObsidianPluginRequest({
|
|
|
111
152
|
reason,
|
|
112
153
|
records_as: "vault_context",
|
|
113
154
|
authoritative_change: false,
|
|
114
|
-
note_paths: notePaths.
|
|
115
|
-
tags: tags.
|
|
155
|
+
note_paths: notePaths.map(assertSafeQueuedPath),
|
|
156
|
+
tags: tags.map(assertSafeQueuedTag),
|
|
116
157
|
queued_at: new Date().toISOString(),
|
|
117
158
|
};
|
|
118
159
|
}
|
|
@@ -284,6 +325,7 @@ export async function indexObsidianVault({ vaultPath, maxExcerptChars = 360 } =
|
|
|
284
325
|
|
|
285
326
|
export async function listObsidianNotes({ vaultPath } = {}) {
|
|
286
327
|
const resolvedVaultPath = await resolveVaultRoot(vaultPath);
|
|
328
|
+
await configureObsidianVault(resolvedVaultPath);
|
|
287
329
|
const notePaths = await listMarkdownNotes(resolvedVaultPath);
|
|
288
330
|
return {
|
|
289
331
|
record_type: "obsidian_note_list",
|
|
@@ -324,7 +366,7 @@ export async function createObsidianNote({ vaultPath, notePath, content, overwri
|
|
|
324
366
|
throw new Error(`Obsidian note already exists: ${target.relativePath}`);
|
|
325
367
|
}
|
|
326
368
|
await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
|
|
327
|
-
await
|
|
369
|
+
await writeVaultNote(target.absolutePath, content, overwrite);
|
|
328
370
|
return createObsidianOperationReceipt({
|
|
329
371
|
operation: overwrite ? "update_note" : "create_note",
|
|
330
372
|
target,
|
|
@@ -337,10 +379,24 @@ export async function updateObsidianNote({ vaultPath, notePath, content } = {})
|
|
|
337
379
|
throw new Error("Obsidian update requires non-empty note content");
|
|
338
380
|
}
|
|
339
381
|
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
|
|
340
|
-
await
|
|
382
|
+
await writeVaultNote(target.absolutePath, content, true);
|
|
341
383
|
return createObsidianOperationReceipt({ operation: "update_note", target, content });
|
|
342
384
|
}
|
|
343
385
|
|
|
386
|
+
async function writeVaultNote(filePath, content, overwrite) {
|
|
387
|
+
const flags =
|
|
388
|
+
fs.constants.O_WRONLY |
|
|
389
|
+
fs.constants.O_CREAT |
|
|
390
|
+
(overwrite ? fs.constants.O_TRUNC : fs.constants.O_EXCL) |
|
|
391
|
+
(fs.constants.O_NOFOLLOW ?? 0);
|
|
392
|
+
const handle = await fs.open(filePath, flags, 0o600);
|
|
393
|
+
try {
|
|
394
|
+
await handle.writeFile(content, "utf8");
|
|
395
|
+
} finally {
|
|
396
|
+
await handle.close();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
344
400
|
export async function deleteObsidianNote({ vaultPath, notePath } = {}) {
|
|
345
401
|
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
|
|
346
402
|
const content = await fs.readFile(target.absolutePath, "utf8");
|
|
@@ -390,7 +446,7 @@ export async function ensureObsidianGraphLinks({ vaultPath } = {}) {
|
|
|
390
446
|
hasCoreNote,
|
|
391
447
|
});
|
|
392
448
|
if (linked === current) continue;
|
|
393
|
-
await
|
|
449
|
+
await writeVaultNote(target.absolutePath, linked, true);
|
|
394
450
|
receipts.push(
|
|
395
451
|
createObsidianOperationReceipt({
|
|
396
452
|
operation: "update_note",
|
|
@@ -416,7 +472,9 @@ export async function appendObsidianOperationReceipt(receipt) {
|
|
|
416
472
|
let log = createDefaultObsidianVaultOperations();
|
|
417
473
|
try {
|
|
418
474
|
log = validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
|
|
419
|
-
} catch {
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error?.code !== "ENOENT") throw error;
|
|
477
|
+
}
|
|
420
478
|
log.operations.push(receipt);
|
|
421
479
|
await writeJson(getObsidianVaultOperationsPath(), log);
|
|
422
480
|
return log;
|
|
@@ -610,6 +668,69 @@ export async function writeObsidianVaultIndex({ vaultPath, ensureGraphLinks = tr
|
|
|
610
668
|
return index;
|
|
611
669
|
}
|
|
612
670
|
|
|
671
|
+
export async function embedProjectContext({ vaultPath, projectIndex } = {}) {
|
|
672
|
+
if (!projectIndex || typeof projectIndex !== "object") {
|
|
673
|
+
throw new Error("Obsidian project context requires a project index");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const resolvedVaultPath = await resolveVaultRoot(vaultPath);
|
|
677
|
+
await configureObsidianVault(resolvedVaultPath);
|
|
678
|
+
const projectName =
|
|
679
|
+
String(projectIndex.project?.name ?? "project")
|
|
680
|
+
.replace(/[^a-zA-Z0-9._ -]/g, "-")
|
|
681
|
+
.trim()
|
|
682
|
+
.replace(/[ .]+$/g, "") || "project";
|
|
683
|
+
const notePath = `Meta-Architect/Projects/${projectName}/Project Context.md`;
|
|
684
|
+
const content = createProjectContextNote(projectIndex);
|
|
685
|
+
const operation = await upsertObsidianNoteContent({
|
|
686
|
+
vaultPath: resolvedVaultPath,
|
|
687
|
+
notePath,
|
|
688
|
+
content,
|
|
689
|
+
});
|
|
690
|
+
if (operation) await appendObsidianOperationReceipt(operation);
|
|
691
|
+
|
|
692
|
+
const index = await writeObsidianVaultIndex({ vaultPath: resolvedVaultPath });
|
|
693
|
+
return {
|
|
694
|
+
vaultPath: resolvedVaultPath,
|
|
695
|
+
notePath,
|
|
696
|
+
noteStatus: operation ? operation.operation : "unchanged",
|
|
697
|
+
index,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export async function appendObsidianLearningRecord({ vaultPath, record } = {}) {
|
|
702
|
+
if (!record || typeof record !== "object") throw new Error("Learning record is required");
|
|
703
|
+
const notePath = "Meta-Architect/Learning Memory.md";
|
|
704
|
+
const existing = await readObsidianNote({ vaultPath, notePath }).catch(
|
|
705
|
+
() => "# Learning Memory\n",
|
|
706
|
+
);
|
|
707
|
+
const lines = [
|
|
708
|
+
`## ${record.created_at ?? new Date().toISOString()}`,
|
|
709
|
+
"",
|
|
710
|
+
`- Claim: ${record.claim}`,
|
|
711
|
+
`- Source: ${record.source}`,
|
|
712
|
+
`- Status: ${record.status}`,
|
|
713
|
+
...(record.failure_state
|
|
714
|
+
? [
|
|
715
|
+
`- Failure state: ${record.failure_state}`,
|
|
716
|
+
`- Resolution status: ${record.resolution_status ?? record.failure_state}`,
|
|
717
|
+
`- Attempted fixes: ${(record.attempted_fixes ?? []).join(", ") || "none"}`,
|
|
718
|
+
]
|
|
719
|
+
: []),
|
|
720
|
+
`- Evidence: ${(record.evidence ?? []).join(", ") || "none"}`,
|
|
721
|
+
`- Files: ${(record.files_involved ?? []).join(", ") || "none"}`,
|
|
722
|
+
`- Next verification: ${record.next_verification}`,
|
|
723
|
+
"",
|
|
724
|
+
];
|
|
725
|
+
await upsertObsidianNoteContent({
|
|
726
|
+
vaultPath,
|
|
727
|
+
notePath,
|
|
728
|
+
content: `${existing.trimEnd()}\n\n${lines.join("\n")}`,
|
|
729
|
+
});
|
|
730
|
+
await ensureObsidianGraphLinks({ vaultPath });
|
|
731
|
+
return notePath;
|
|
732
|
+
}
|
|
733
|
+
|
|
613
734
|
async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
|
|
614
735
|
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
|
|
615
736
|
let current = null;
|
|
@@ -619,7 +740,7 @@ async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
|
|
|
619
740
|
if (current === content) return null;
|
|
620
741
|
|
|
621
742
|
await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
|
|
622
|
-
await
|
|
743
|
+
await writeVaultNote(target.absolutePath, content, Boolean(current));
|
|
623
744
|
return createObsidianOperationReceipt({
|
|
624
745
|
operation: current === null ? "create_note" : "update_note",
|
|
625
746
|
target,
|
|
@@ -663,6 +784,53 @@ These linked notes are vault_context. They are not build_evidence. Build evidenc
|
|
|
663
784
|
`;
|
|
664
785
|
}
|
|
665
786
|
|
|
787
|
+
function createProjectContextNote(projectIndex) {
|
|
788
|
+
const project = projectIndex.project ?? {};
|
|
789
|
+
const lines = [
|
|
790
|
+
`# ${project.name ?? "Project"}`,
|
|
791
|
+
"",
|
|
792
|
+
"This note is Meta-Architect vault context, not build evidence.",
|
|
793
|
+
"",
|
|
794
|
+
`- Graph hub: ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}`,
|
|
795
|
+
`- Source authority: ${projectIndex.authority ?? "source_truth"}`,
|
|
796
|
+
`- Freshness: ${projectIndex.freshness?.status ?? "unknown"}`,
|
|
797
|
+
`- Source files scanned: ${projectIndex.quality?.coverage?.sourceFiles ?? 0}`,
|
|
798
|
+
`- Context quality: ${projectIndex.quality?.completeness ?? "unknown"} / ${projectIndex.quality?.confidence ?? "unknown"}`,
|
|
799
|
+
`- Canonical facts: ${(projectIndex.facts ?? []).map((fact) => fact.id).join(", ") || "none"}`,
|
|
800
|
+
"",
|
|
801
|
+
"## Stack",
|
|
802
|
+
"",
|
|
803
|
+
`- Languages: ${(projectIndex.languages ?? []).join(", ") || "unknown"}`,
|
|
804
|
+
`- Frameworks: ${(projectIndex.frameworks ?? []).join(", ") || "unknown"}`,
|
|
805
|
+
`- Package manager: ${projectIndex.packageManager ?? "unknown"}`,
|
|
806
|
+
"",
|
|
807
|
+
"## Commands",
|
|
808
|
+
"",
|
|
809
|
+
...Object.entries(projectIndex.commands ?? {}).map(
|
|
810
|
+
([name, command]) => `- ${name}: \`${command}\``,
|
|
811
|
+
),
|
|
812
|
+
"",
|
|
813
|
+
"## Entry points and documentation",
|
|
814
|
+
"",
|
|
815
|
+
...((projectIndex.entrypoints ?? []).length
|
|
816
|
+
? projectIndex.entrypoints.map((entry) => `- ${entry}`)
|
|
817
|
+
: ["- No entry points detected."]),
|
|
818
|
+
...((projectIndex.importantDocs ?? []).length
|
|
819
|
+
? projectIndex.importantDocs.map((entry) => `- ${entry}`)
|
|
820
|
+
: ["- No important documentation detected."]),
|
|
821
|
+
"",
|
|
822
|
+
"## Agent integrations",
|
|
823
|
+
"",
|
|
824
|
+
`- ${(projectIndex.vendorIntegrations ?? []).join(", ") || "None detected"}`,
|
|
825
|
+
"",
|
|
826
|
+
];
|
|
827
|
+
return createManagedMarkdownBlock({
|
|
828
|
+
id: "obsidian-project-context",
|
|
829
|
+
source: ".ma/context/project-index.json",
|
|
830
|
+
body: lines.join("\n"),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
666
834
|
function withObsidianGraphSection(content, { currentPath, hasCoreNote }) {
|
|
667
835
|
const base = content
|
|
668
836
|
.replace(new RegExp(`\\n${escapeRegExp(obsidianGraphSectionHeading)}[\\s\\S]*$`, "m"), "")
|
|
@@ -705,15 +873,7 @@ function createObsidianOperationReceipt({ operation, target, content }) {
|
|
|
705
873
|
}
|
|
706
874
|
|
|
707
875
|
async function resolveVaultRoot(vaultPath) {
|
|
708
|
-
|
|
709
|
-
throw new Error("Obsidian vault operation requires vaultPath");
|
|
710
|
-
}
|
|
711
|
-
const resolvedVaultPath = path.resolve(vaultPath);
|
|
712
|
-
const stat = await fs.stat(resolvedVaultPath);
|
|
713
|
-
if (!stat.isDirectory()) {
|
|
714
|
-
throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
|
|
715
|
-
}
|
|
716
|
-
return resolvedVaultPath;
|
|
876
|
+
return resolveObsidianVault(vaultPath);
|
|
717
877
|
}
|
|
718
878
|
|
|
719
879
|
async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
|
|
@@ -721,7 +881,10 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
|
|
|
721
881
|
if (!notePath || typeof notePath !== "string") {
|
|
722
882
|
throw new Error("Obsidian note operation requires notePath");
|
|
723
883
|
}
|
|
724
|
-
const normalizedInput =
|
|
884
|
+
const normalizedInput = path.posix
|
|
885
|
+
.normalize(normalizeVaultPath(notePath.trim()))
|
|
886
|
+
.replace(/^(\.\/)+/, "")
|
|
887
|
+
.replace(/^\/+/, "");
|
|
725
888
|
if (
|
|
726
889
|
!normalizedInput ||
|
|
727
890
|
normalizedInput.startsWith("../") ||
|
|
@@ -740,6 +903,7 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
|
|
|
740
903
|
const relativePath = normalizedInput.toLowerCase().endsWith(".md")
|
|
741
904
|
? normalizedInput
|
|
742
905
|
: `${normalizedInput}.md`;
|
|
906
|
+
await rejectSymlinkComponents(resolvedVaultPath, relativePath);
|
|
743
907
|
const absolutePath = path.resolve(resolvedVaultPath, relativePath);
|
|
744
908
|
if (!absolutePath.startsWith(`${resolvedVaultPath}${path.sep}`)) {
|
|
745
909
|
throw new Error("Obsidian note path resolved outside the vault");
|
|
@@ -747,9 +911,53 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
|
|
|
747
911
|
if (mustExist && !(await pathExists(absolutePath))) {
|
|
748
912
|
throw new Error(`Obsidian note does not exist: ${relativePath}`);
|
|
749
913
|
}
|
|
914
|
+
try {
|
|
915
|
+
if ((await fs.lstat(absolutePath)).isSymbolicLink()) {
|
|
916
|
+
throw new Error("Obsidian note path cannot be a symlink");
|
|
917
|
+
}
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (error?.code !== "ENOENT") throw error;
|
|
920
|
+
}
|
|
921
|
+
const resolvedTarget = await resolveExistingPathOrParent(absolutePath);
|
|
922
|
+
if (
|
|
923
|
+
resolvedTarget !== resolvedVaultPath &&
|
|
924
|
+
!resolvedTarget.startsWith(`${resolvedVaultPath}${path.sep}`)
|
|
925
|
+
) {
|
|
926
|
+
throw new Error("Obsidian note path resolves through a symlink outside the vault");
|
|
927
|
+
}
|
|
750
928
|
return { vaultPath: resolvedVaultPath, relativePath, absolutePath };
|
|
751
929
|
}
|
|
752
930
|
|
|
931
|
+
async function resolveExistingPathOrParent(filePath) {
|
|
932
|
+
let candidate = filePath;
|
|
933
|
+
while (candidate !== path.dirname(candidate)) {
|
|
934
|
+
try {
|
|
935
|
+
return await fs.realpath(candidate);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (error?.code !== "ENOENT") {
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
candidate = path.dirname(candidate);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return fs.realpath(candidate);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function rejectSymlinkComponents(rootPath, relativePath) {
|
|
947
|
+
let current = rootPath;
|
|
948
|
+
for (const component of relativePath.split("/")) {
|
|
949
|
+
current = path.join(current, component);
|
|
950
|
+
try {
|
|
951
|
+
if ((await fs.lstat(current)).isSymbolicLink()) {
|
|
952
|
+
throw new Error("Obsidian note path cannot traverse a symlink");
|
|
953
|
+
}
|
|
954
|
+
} catch (error) {
|
|
955
|
+
if (error?.code !== "ENOENT") throw error;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
753
961
|
async function pathExists(filePath) {
|
|
754
962
|
try {
|
|
755
963
|
await fs.access(filePath);
|
|
@@ -834,6 +1042,36 @@ function normalizeLinkKey(value) {
|
|
|
834
1042
|
.toLowerCase();
|
|
835
1043
|
}
|
|
836
1044
|
|
|
1045
|
+
function assertSafeQueuedPath(value) {
|
|
1046
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1047
|
+
throw new Error("Obsidian plugin request path must be a non-empty string");
|
|
1048
|
+
}
|
|
1049
|
+
const normalized = normalizeVaultPath(value.trim()).replace(/^\/+/, "");
|
|
1050
|
+
if (
|
|
1051
|
+
normalized.startsWith("../") ||
|
|
1052
|
+
normalized.includes("/../") ||
|
|
1053
|
+
normalized === ".." ||
|
|
1054
|
+
normalized.startsWith(".obsidian/") ||
|
|
1055
|
+
normalized.startsWith(".trash/") ||
|
|
1056
|
+
normalized.startsWith(".git/") ||
|
|
1057
|
+
normalized.startsWith(".ma/")
|
|
1058
|
+
) {
|
|
1059
|
+
throw new Error(`Obsidian plugin request path is not allowed: ${normalized}`);
|
|
1060
|
+
}
|
|
1061
|
+
return normalized;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function assertSafeQueuedTag(value) {
|
|
1065
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1066
|
+
throw new Error("Obsidian plugin request tag must be a non-empty string");
|
|
1067
|
+
}
|
|
1068
|
+
const normalized = value.trim().replace(/^#/, "");
|
|
1069
|
+
if (!/^[A-Za-z0-9_/-]+$/.test(normalized)) {
|
|
1070
|
+
throw new Error(`Obsidian plugin request tag is not allowed: ${normalized}`);
|
|
1071
|
+
}
|
|
1072
|
+
return normalized;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
837
1075
|
function escapeRegExp(value) {
|
|
838
1076
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
839
1077
|
}
|